2、RESTful API设计规范:资源命名、HTTP方法语义、状态码选择
好,咱们今天来聊一个看似基础、但实际上坑特别多的主题——RESTful API 设计规范。
我见过太多团队,接口写得随心所欲。
同一个系统里,getUser、query_user、/user/query 全混着来。
结果呢?前端对接痛苦,后期维护更是一团乱麻。
说白了,规范不是束缚,是解放生产力。
我自己的习惯,先把三个核心支柱讲清楚:资源怎么命名、动词怎么用、状态码怎么选。
这三件事搞明白了,你的接口设计至少能打 80 分。
2.1 资源命名:统一、清晰、可读
先看一个反面例子。
GET /getUserList
POST /addNewUser
DELETE /removeUserById?id=123
这么写有什么问题?
第一,动词和名词混在一起,URL 变成了“操作说明书”。
第二,大小写不一致,getUserList 和 addNewUser 风格不统一。
第三,查询参数直接拼在路径里,没法缓存。
RESTful 规范怎么说?
用名词复数表示资源,用HTTP 方法表示操作。
路径里只放资源标识,不放动作。
GET /users // 获取用户列表
POST /users // 创建新用户
GET /users/123 // 获取单个用户
PUT /users/123 // 全量更新用户
DELETE /users/123 // 删除用户
你看,一眼就知道在操作什么。
我个人还坚持几条规则:
- 全部小写,避免大小写敏感带来的歧义。
/UserInfo和/userinfo是两个不同的路径,这种坑我踩过。 - 用连字符
-代替下划线_。下划线在某些浏览器和搜索引擎里会被截断或误解。连字符更友好。 - 避免过深的层级。三级就够了,超过五级说明设计有问题。比如
/users/123/orders/456/items/789,太深了,可以考虑用查询参数。 - 路径中不要带文件扩展名。
/users.json是坏味道。通过Accept头部来做内容协商。
关于子资源的处理,我建议用 /父资源/{id}/子资源 的格式。
比如,某个用户的订单:/users/123/orders。
但如果子资源本身就是独立业务实体,你完全可以给它独立路径。
我在项目中遇到过,有人非要把所有东西都嵌套在用户下面,结果路径长到离谱,缓存策略都没法做。
2.2 HTTP 方法语义:别把 POST 当万能钥匙
HTTP 协议给了我们一堆方法,很多人只用 GET 和 POST。
你想想看,PUT、DELETE、PATCH 难道是摆设吗?
核心原则:方法要和动作匹配。
| 方法 | 语义 | 幂等 | 安全 | 典型场景 |
|---|---|---|---|---|
| GET | 查询资源 | 是 | 是 | 获取列表、详情 |
| POST | 创建资源 / 提交数据 | 否 | 否 | 创建订单、上传文件 |
| PUT | 全量替换资源 | 是 | 否 | 更新用户全部信息 |
| PATCH | 部分更新资源 | 否 | 否 | 只修改用户昵称 |
| DELETE | 删除资源 | 是 | 否 | 删除用户 |
这里 幂等 和 安全 是两个容易被误解的概念。
- 安全:不会改变服务端状态。GET 就是典型的“只读”操作。
- 幂等:同一请求执行多次,结果和只执行一次一样。PUT 和 DELETE 是幂等的,但 POST 不是。
为什么这个区分重要?
因为重试策略。如果你在网络超时后重试一个 POST 请求,可能会产生重复的订单。
而重试 GET 或 PUT,就没这个风险。
我来讲一个避坑的例子。
我曾经接手过一个项目,所有写操作都用 POST。
更新用户信息?POST。删除记录?POST。
后面前端做失败重试,导致用户数据被重复创建了好几次。
我当时盯着数据库里多出来的三条记录,心里默默叹了口气。
所以,该用 PUT 就 PUT,该用 DELETE 就 DELETE,别偷懒。
关于 PATCH 和 PUT 的区别,我多说两句。
PUT 要求客户端发送完整的资源表示。
比如更新用户时,即使只改邮箱,也要带上用户名、手机号所有字段。
PATCH 则只需要发送差异部分。
// PUT:全量更新
PUT /users/123
{
"name": "张三",
"email": "new@example.com",
"phone": "13800138000"
}
// PATCH:部分更新
PATCH /users/123
{
"email": "new@example.com"
}
我个人建议,如果资源字段很少(比如 3-5 个),直接用 PUT 省事。
但如果字段很多,或者更新操作很随机,PATCH 更合适。
还有一个容易忽略的点:POST 也可以用于“创建不属于明确父资源的资源”。
比如登录认证:POST /auth/login,这里的 /auth 更像一个命名空间,不是资源集合。
这种用法不算违反规范,但不要滥用。
2.3 状态码选择:别只会上 200 和 500
说到状态码,我发现很多团队挺“懒”的。
成功全部返回 200,失败统一返回 500。
前端接到 500,根本不知道是参数错了,还是没权限,还是服务器挂了。
还得再解析一遍响应体里的 code 字段。
这不就是“脱裤子放屁”吗?
HTTP 协议本身已经提供了丰富的状态码,为什么不用?
我列一下最常用的:
| 范围 | 状态码 | 含义 | 常见场景 |
|---|---|---|---|
| 2xx 成功 | 200 | 请求成功 | GET、PUT 成功返回 |
| 201 | 创建成功 | POST 成功后 | |
| 202 | 已接受(异步处理) | 后台任务提交 | |
| 204 | 无内容 | DELETE 成功 | |
| 3xx 重定向 | 301 | 永久重定向 | 接口迁移 |
| 302 | 临时重定向 | 未登录跳转登录 | |
| 4xx 客户端错误 | 400 | 请求参数错误 | 校验失败、JSON 格式错误 |
| 401 | 未认证 | 缺少 或 过期 | |
| 403 | 无权限 | 已认证但不能操作 | |
| 404 | 资源不存在 | 路径或 ID 错误 | |
| 405 | 方法不允许 | 用了 POST 但只支持 GET | |
| 409 | 冲突 | 重复创建、版本冲突 | |
| 422 | 请求语义错误 | 业务规则不满足 | |
| 429 | 请求过频 | 限流触发 | |
| 5xx 服务端错误 | 500 | 服务器内部错误 | 未捕获异常 |
| 502 | 网关错误 | 上游服务宕机 | |
| 503 | 服务不可用 | 过载降级、维护中 |
我来说几个重点场景:
- DELETE 成功后返回 204,不要返回 200 带个空对象。前端不需要响应体。
我以前见过 DELETE 返回 200 +{ "success": true },完全是多余的。
204 既省带宽,语义又明确。 - POST 创建成功后应该返回 201,并在响应头
Location中给出新资源的 URL。
比如Location: /users/124。
为什么?这样客户端可以直接拿到新资源的地址,不需要额外解析。 - 400 和 422 的区别。
400 通常表示客户端发送了语法错误的数据,比如 JSON 解析失败。
422 表示数据格式正确,但不满足业务约束,比如用户名已存在、邮箱格式不对。
我个人更推荐把所有校验失败都返回 422,因为 400 的语义太宽泛,不利于排查。
{ "error": { "code": "USERNAME_EXISTS", "message": "用户名已被注册" } }这样既用了 HTTP 状态码表明类别,又在 body 里提供了精准的错误码,方便前端做国际化或针对性处理。
再说一个很多人都忽略的点:别滥用 500。
如果是因为客户端输入错误导致的服务异常,那是你的代码没做好校验。
正确的做法是,在入口处拦截校验,返回 4xx,而不是让请求穿透到业务层,最后抛一个 500 出来。
我曾经遇到过,一个 400 的错误因为没有校验,直接导致了空指针异常,服务端返回了 500。
排查了半小时,结果发现是前端传了个 null。
这种问题,一个简单的参数校验就能避免。
2.4 三者关系:一张图说明白
下面这张图总结了我上面讲的内容。
资源命名是骨架,HTTP 方法是动作,状态码是反馈。
三者配合,才能构建出清晰、可维护的 API。
规范是死的,业务是活的。
比如,有时候为了兼容历史版本,你不得不用 POST 做查询(因为 GET 请求体在某些网关里会被丢弃)。
这时候可以折中,但一定要在文档里注释清楚。
记住,一致性比完美性更重要。
嗯,以上就是 RESTful API 设计的核心规范。
你可能会觉得这些规则不难,人人都懂。
但“懂”和“做”之间,差的是落地和坚持。