RESTful API 设计:URL 不放动词、错误不返 200,以及名词化吵不出结果的几类接口
底稿:阮一峰《RESTful API 最佳实践》,另整理其评论区里几类长期没有统一答案的接口。
一、URL 设计
用「动词 + 宾语」表达操作
客户端对数据的操作指令是「动词 + 宾语」。GET /articles 里,GET 是动词,/articles 是宾语。
五种 HTTP 方法与 CRUD 的对应关系:
- GET:读取
- POST:新建
- PUT:更新(全量)
- PATCH:更新(部分)
- DELETE:删除
HTTP 方法一律大写。
动词覆盖
有些客户端只能发 GET 和 POST。服务器要接受用 POST 模拟 PUT / PATCH / DELETE,客户端加请求头:
POST /api/Person/4 HTTP/1.1
X-HTTP-Method-Override: PUT
宾语必须是名词
URL 是动词作用的对象,应该是名词。
- 正确:
/articles - 错误:
/getAllCars、/createNewCar、/deleteAllRedCars
用复数 URL
建议统一用复数:GET /articles/2 好于 GET /article/2。
避免多级 URL
GET /authors/12/categories/2 不利于扩展,语义也不明。除第一级外,其他维度用查询字符串:
GET /authors/12?categories=2
查询已发布的文章用 GET /articles?published=true,而不是 GET /articles/published。
二、状态码
状态码分五大类:1xx 信息、2xx 成功、3xx 重定向、4xx 客户端错误、5xx 服务器错误。API 不需要 1xx。
2xx
- GET → 200
- POST → 201 Created
- PUT → 200
- PATCH → 200
- DELETE → 204 No Content
202 Accepted 表示请求已收到、但异步处理:
{"task": {"href": "...", "id": "2130040"}}
3xx
API 基本用不到 301 / 302 / 307,主要是 303 See Other,用于 POST / PUT / DELETE 之后的重定向,配合 Location 头:
HTTP/1.1 303 See Other
Location: /api/orders/12345
4xx
400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found、405 Method Not Allowed、410 Gone、415 Unsupported Media Type、422 Unprocessable Entity、429 Too Many Requests。
5xx
一般只要 500 Internal Server Error 和 503 Service Unavailable,不向用户暴露服务器细节。
三、服务器回应
不要返回纯文本
返回 JSON 对象,响应头带 Content-Type: application/json;请求方发 Accept: application/json。
发生错误不要返回 200
错误也返 200、把错误塞进 body:
{"status": "failure", "data": {"error": "..."}}
这等于取消了状态码。正确做法是状态码反映错误、错误详情放 body:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"error": "Invalid payload.", "detail": {"surname": "This field is required."}}
提供链接 HATEOAS
响应里给出相关链接,客户端只记一个入口就能发现其他 URL(GitHub 的 api.github.com 就是这种做法)。更好的写法是把 links 与其他属性分开:
{
"status": "In progress",
"links": [
{"rel": "cancel", "method": "delete", "href": "..."},
{"rel": "edit", "method": "put", "href": "..."}
]
}
四、名词化吵不出结果的场景
纯动作类接口
下单、支付、退订、启用 / 禁用、验证码校验。名词化都很难:
- 一种主张是
POST /orders/下单、PUT /users/{id}?status=enable - 另一种主张是拆成资源:
POST /orders、PUT /orders/{id} - GitHub 用的是
/datas/1/action这种形式
没有统一结论,选一种并在团队内保持一致。
检查类接口
用户名是否已存在,可以理解为「尝试获取」:GET /users/{name},不存在就返回 404;也可以写成 /users/{name}?c=check。
批量操作
PUT /users/{id}/enable 只适合单个。批量要另设集合级接口,GitHub 用 actions 前缀:/actions/:action/resources,例如 /actions/restart/servers。
HTTP 状态码 vs 业务错误码
一派认为 HTTP 状态码表达传输 / 协议层错误,业务逻辑错误(用户名密码不匹配、余额不足)用 body 里的 code 字段约定;另一派主张所有错误都体现在 HTTP 状态码上。多数人的做法是两者并存:HTTP 状态码给通用语义,body 带业务 code。
跨域取不到非 200 状态码
错。只要基于 HTTP,前端就能拿到状态码;拿不到是没正确配置 CORS 暴露头,不是要把错误都返 200。
错误返回 200
有人觉得这样后端少判断一步。实际结果是缓存、重试、监控全部失效。