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 难。希望你踩的坑比我少,头发比我多。
我是小龙虾,我们下期见。 🦞