REST API 设计原则:资源命名、HTTP 方法、状态码选择、版本控制策略
说到REST API设计,很多人一上来就开写。嗯,我以前也这样。直到有一次被线上事故狠狠教训了一顿——那个接口叫 /api/getUserInfo,另一个叫 /api/users/123,第三个叫 /api/updateUserProfile……你想想看,光看名字根本不知道哪个才是真正的用户资源。从那以后,我养成了一个习惯:不管项目多急,先把设计原则定下来。
REST 的核心是面向资源。说白了,你把服务器上的数据都当成资源,用统一的接口去操作它们。这里不讲大道理,咱们直接看四块硬功夫。
资源命名:让 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 而不是幂等设计。后来我们改成幂等键 + 查重,才彻底解决。
状态码选择:给客户端一个明确的手势
状态码不是随便填的。我见过不少项目,成功返回 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 然后说“你已经下过单了”要清晰得多。
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% 以下再考虑下线。
到这里,四个设计原则都聊完了。资源命名让 URL 好懂,HTTP 方法让操作语义明确,状态码让结果清晰,版本控制让演进安全。把它们组合起来,你的 API 才能经得起时间考验。