REST API 设计原则:资源命名、HTTP 方法、状态码选择、版本控制策略

说到REST API设计,很多人一上来就开写。嗯,我以前也这样。直到有一次被线上事故狠狠教训了一顿——那个接口叫 /api/getUserInfo,另一个叫 /api/users/123,第三个叫 /api/updateUserProfile……你想想看,光看名字根本不知道哪个才是真正的用户资源。从那以后,我养成了一个习惯:不管项目多急,先把设计原则定下来。

REST 的核心是面向资源。说白了,你把服务器上的数据都当成资源,用统一的接口去操作它们。这里不讲大道理,咱们直接看四块硬功夫。

REST API 设计原则核心框架 REST API 设计原则 资源命名 HTTP 方法 状态码选择 版本控制策略 四个原则互相关联:好的资源命名让 URL 清晰,正确的 HTTP 方法 + 状态码让语义一致,版本控制保证演进 我建议在项目初期就把这四块固定下来,后期改动的成本远高于前期设计。

资源命名:让 URL 自己会说话

资源命名这件事,看着简单,踩坑的人却不少。我见过一个项目,接口列表里有 /getAllUsers、/usersList、/queryUser……三个接口查的都是同一张表。为什么要统一?因为客户端代码会写成五花八门,运维维护API网关时想死的心都有。

我个人习惯遵循两条铁律:

  • 用名词复数:/users 而不是 /user 或 /getUsers。复数代表资源集合,语义最干净。
  • 层次用斜杠表示从属:比如 /users/123/orders 表示“用户 123 下的订单集合”。不要搞 /getOrders?userId=123 这种伪 RPC 写法。
一点小技巧:关联资源超过两级时,我建议你考虑是否需要扁平化。/users/123/orders/456/items 读起来累,查询性能也差。不如拆成两个接口:/orders/456 + 查询参数 ?userId=123。

另外,不要用动词。REST 不是 RPC。动作应该由 HTTP 方法表达,而不是 URL。比如 POST /orders 就是创建订单,别写成 POST /createOrder。

HTTP 方法:别把 PUT 当 PATCH 用

HTTP 方法一共就那么几个,但用错的现象非常普遍。我记得有一次做代码评审,有人用 POST /users/123 来做更新操作。问他为什么不用 PUT,他说“POST 也能传数据啊……”那一刻我意识到,很多人对 HTTP 语义的理解是模糊的。

这里我列一个常用对照表:

方法语义典型 URL幂等注意
GET获取资源GET /users/123✅不应修改数据
POST创建资源POST /users❌服务端分配 ID
PUT全量替换PUT /users/123✅客户端指定完整资源
PATCH部分更新PATCH /users/123✅(通常)只传需要改的字段
DELETE删除资源DELETE /users/123✅删除后 GET 返回 404

为什么我强调幂等?因为网络重试时,POST 重试会创建多个资源,而 PUT 重试不会造成额外影响。我在项目中遇到过支付回调重复扣款的例子,原因就是回调接口用 POST 而不是幂等设计。后来我们改成幂等键 + 查重,才彻底解决。

我曾经踩过的坑:一个接口同时支持创建和更新,用 POST 包打天下。结果前端分不清什么时候该调哪个方法,导致同一条数据被重复插入。记住:每种方法只做一件事。

状态码选择:给客户端一个明确的手势

状态码不是随便填的。我见过不少项目,成功返回 200,失败也返回 200,然后在 body 里放一个 code: 0 表示成功,code: 1 表示失败。这么做等于把 HTTP 层的能力废掉了。你想想看,网关、负载均衡、代理缓存全都依赖状态码做决策,你把错误信息塞进 Body,它们根本感知不到。

我的原则很简单:

  • 成功:200 OK(GET)、201 Created(POST)、204 No Content(DELETE)
  • 客户端问题:400 Bad Request(参数错误)、401 Unauthorized(未认证)、403 Forbidden(没权限)、404 Not Found、409 Conflict(资源冲突)
  • 服务端问题:500 Internal Server Error、502 Bad Gateway、503 Service Unavailable

特别说一下 409 Conflict。我当初第一次用它是在一个订单防重复提交的场景。客户端发起的创建请求如果和已有订单冲突,返回 409 并附带冲突原因。比返回 200 然后说“你已经下过单了”要清晰得多。

推荐做法:错误响应 body 统一用 JSON,包含 error、message、details 字段。例如:
{
  "error": "USERNAME_EXISTS",
  "message": "用户名已被注册",
  "details": { "field": "username" }
}
这样客户端不用解析多种格式,一个 switch 就能处理。

版本控制策略:别让老客户端断掉

API 版本控制这件事,很多人觉得“先上线再说,有问题再改”。但现实是,一旦移动端 App 发布了,你改接口就寸步难行。我经历过一个惨痛的教训:把 /users/123 返回的 name 字段改成了 fullName,结果 iOS 和 Android 同时崩溃——因为它们解析的是 name。

目前主流的有三种方式:

方式示例优点缺点
URL 路径版本/v1/users/123最直观,易于缓存版本号污染 URL
请求头版本Accept: application/vnd.company+json;version=1干净,符合 REST 语义调试不方便,网关解析复杂
查询参数版本/users/123?version=1实现简单缓存不友好,容易混乱

我个人更倾向 URL 路径版本,原因很简单:一眼就能看出来是哪个版本,而且 CDN 和网关缓存按 URL 区分,不会串。处理老版本时,我会在 /v1 下保留旧逻辑,新接口走 /v2,等老客户端升级率降到 5% 以下再考虑下线。

一个小提示:版本号应该是整数(v1、v2),不要用小版本。小版本应该通过兼容性策略处理,而不是每个都开一个新的 URL 前缀。我习惯给每个版本设定一个生命周期:v1 从上线起支持 12 个月,到期前 3 个月发公告。

到这里,四个设计原则都聊完了。资源命名让 URL 好懂,HTTP 方法让操作语义明确,状态码让结果清晰,版本控制让演进安全。把它们组合起来,你的 API 才能经得起时间考验。