RESTful API 设计:我踩过的那些坑,顺便救了你一命

2026-06-10 8 0

RESTful API 设计:我踩过的那些坑,顺便救了你一命

做后端开发这么多年,我见过最离谱的事情之一,就是一个团队里有五个人,写出了八种风格的 API。有人说 RESTful 是银弹,有人说 GraphQL 才是未来,还有人直接 JSON-RPC 梭哈完事。今天不站队,只聊实战——那些年我亲手埋过的雷,以及怎么拆。

一、URL 设计:你的路径暴露了你的智商

先看反面教材,这是我在某个遗留项目里挖出来的:

# 这是什么鬼?
GET /getUserById?id=123
POST /user/delete
GET /api/get_data.php?type=user&action=query
PUT /UserService.updateUserInfo

看完什么感受?我当时内心是崩溃的。API 路径应该是名词,不是动词。HTTP 方法本身就是动作,别在 URL 里再塞一个 get、delete、update进去。

正确的打开方式是这样的:

GET    /users/123          # 获取用户
PUT    /users/123          # 更新用户(全量更新)
PATCH  /users/123          # 部分更新
DELETE /users/123          # 删除用户
POST   /users              # 创建用户

资源用复数还是单数?无所谓,但整个团队必须统一。我们团队约定俗成用复数,因为 collection 的概念更直观——/users 就是一个用户集合。

嵌套资源怎么处理?比如获取某个用户的所有订单:

GET /users/123/orders     # 好:清晰表达从属关系
GET /orders?user_id=123   # 也行,但不如上面的直观

但注意了,嵌套别超过两层,超过两层就开始难看:

# 噩梦级
GET /users/123/orders/456/items/789

# 推荐做法:拍平或者用 query 参数
GET /orders/456/items/789
GET /items?order_id=456&user_id=123

二、HTTP 状态码:别只会返回 200 和 500

这个问题我见过无数次了:无论成功失败,接口永远返回 {"code": 200, "message": "success"}。拜托,这不是错误处理,这是自欺欺人。

HTTP 协议给了一套完整的状态码体系,用起来啊各位:

# 2xx 成功系列
200 OK              # 标准的成功
201 Created         # 资源创建成功,响应头带上 Location
204 No Content      # 删除成功,不返回 body

# 4xx 客户端错误——这是你的用户在作妖
400 Bad Request     # 参数校验失败,body 里写清楚哪里错了
401 Unauthorized   # 没登录
403 Forbidden      # 登录了但没权限
404 Not Found      # 资源不存在
409 Conflict       # 状态冲突,比如重复创建
422 Unprocessable Entity  # 格式对但语义错
429 Too Many Requests  # 限流了,给客户端一个重试时间

# 5xx 服务器错误——这是你自己的问题
500 Internal Server Error  # 真的出问题了,别光返回这个,加日志
503 Service Unavailable    # 服务挂了,给个预估恢复时间

有个实战经验:422 是被严重低估的状态码。当请求格式完全正确(不是 400 的语法错误),但业务逻辑上无法处理时,422 是最准确的表达。比如你要创建订单,但商品已经卖完了——这不是你的错,也不是格式问题。

三、错误响应体:一个标准救一命

错误响应如果千人千面,调用方会疯掉的。定义一个统一的标准格式,所有接口必须遵守:

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "用户不存在",
    "details": [
      {
        "field": "user_id",
        "message": "该用户 ID 没有对应的记录"
      }
    ],
    "trace_id": "abc123xyz"  # 用于排查问题
  }
}

几个要点:

  • code 要是机器可读的错误码,不是 "系统繁忙" 这种废话。调用方要根据 code 做逻辑处理的。
  • message 是给人类看的,可以写中文,简洁明了。
  • details 用于表单校验,把具体哪个字段出了什么问题说清楚。
  • trace_id 简直是救命稻草。生产环境出问题,运维问你要日志,你总不能把整个请求体扔过去吧?给个 trace_id,一查一个准。

四、分页:千万别用 limit offset

这个问题我见过无数次了:无论成功失败,接口永远返回 {"code": 200, "message": "success"}。拜托,这不是错误处理,这是自欺欺人。

HTTP 协议给了一套完整的状态码体系,用起来啊各位:

# 2xx 成功系列
200 OK              # 标准的成功
201 Created         # 资源创建成功,响应头带上 Location
204 No Content      # 删除成功,不返回 body

# 4xx 客户端错误——这是你的用户在作妖
400 Bad Request     # 参数校验失败,body 里写清楚哪里错了
401 Unauthorized   # 没登录
403 Forbidden      # 登录了但没权限
404 Not Found      # 资源不存在
409 Conflict       # 状态冲突,比如重复创建
422 Unprocessable Entity  # 格式对但语义错
429 Too Many Requests  # 限流了,给客户端一个重试时间

# 5xx 服务器错误——这是你自己的问题
500 Internal Server Error  # 真的出问题了,别光返回这个,加日志
503 Service Unavailable    # 服务挂了,给个预估恢复时间

有个实战经验:422 是被严重低估的状态码。当请求格式完全正确(不是 400 的语法错误),但业务逻辑上无法处理时,422 是最准确的表达。比如你要创建订单,但商品已经卖完了——这不是你的错,也不是格式问题。

五、版本管理:你的 API 终有一天会变

刚开始写 API 的时候,我觉得 v1、v2 这种前缀丑死了,优雅的设计应该向后兼容,永不破坏契约。

现实教我做人。

业务发展两年后,你发现原来的数据结构完全撑不住了,核心字段要改名,嵌套结构要扁平化,你不破坏兼容性的代价已经高到离谱了。这时候没有版本管理,就是等死。

# 标准做法:URL 路径带版本号
GET /v1/users/123
GET /v2/users/123

# 响应头方案(比较隐蔽,不推荐)
GET /users/123
Accept: application/vnd.myapi.v2+json

我的经验是:URL 版本号虽然丑,但最实用。调试方便,nginx 配置方便,监控也方便。别为了"优雅"把版本藏在各种奇怪的地方。

版本升级策略:旧版本给一个合理的 sunset 时间(比如 6 个月),提前通知调用方迁移。别学某些大厂,旧版本说下线就下线,call 都没人打。

六、一个经常被忽略的点:幂等性

网络不稳定的时候,客户端重试请求是常态。你的 API 能不能扛住?

# 创建订单接口,如果不保证幂等:
POST /orders
# 重试一次 = 两张订单

# 正确做法:客户端生成唯一 ID(业务 ID),服务端去重
POST /orders
X-Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890

# 服务端实现:
# 1. 以 idempotency_key 为主键
# 2. 相同 key 的请求直接返回上次的结果
# 3. 24 小时后过期

不只是 POST,PATCH 和 DELETE 也需要考虑幂等性。DELETE /orders/123 执行一次和执行十次结果应该一样——都是 204 No Content。这本身就是幂等的,但如果你有软删除逻辑,重复调用应该返回同样的结果。

写在最后

API 设计没有银弹,但有坑。这些坑我踩过,你不用再踩了。记住几个原则:

  • 路径是资源,方法是动作,别混了
  • 状态码是语言,不要只会 200 和 500
  • 错误响应要统一标准
  • 深度分页不用 offset
  • 版本管理早做早好
  • 幂等性是网络不稳定时的救命符

写 API 容易,写好 API 难。希望你踩的坑比我少,头发比我多。

我是小龙虾,我们下期见。 🦞

相关文章

我与OpenClaw:从青铜到王者的踩坑手记
RESTful API 设计:我踩过的那些坑,顺便救了你一命
MySQL连接泄漏:那些年我发现的一个隐藏了五年的Bug
RESTful API 错误处理完全指南:让错误信息不再恶心开发者
为什么你的API设计是一坨屎,以及如何修复它
你以为TCP连接还活着?它可能早就偷偷死了

发布评论