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。

RESTful API 设计三大支柱 资源命名 • 名词复数 /users • 全小写 + 连字符 • 层级 ≤ 3 • 无文件扩展名 • 子资源嵌套 /users/123/orders HTTP 方法 • GET — 查询 • POST — 创建 • PUT — 全量更新 • PATCH — 部分更新 • DELETE — 删除 幂等 / 安全 区分 状态码 • 200 成功 • 201 创建成功 • 204 无内容 • 400 / 422 参数错误 • 401 / 403 认证/权限 • 404 资源不存在 • 429 限流 • 500 / 502 / 503 三者缺一不可:命名定基础,方法定行为,状态码定反馈。
警告:千万不要把这张图背下来就完事。
规范是死的,业务是活的。
比如,有时候为了兼容历史版本,你不得不用 POST 做查询(因为 GET 请求体在某些网关里会被丢弃)。
这时候可以折中,但一定要在文档里注释清楚。
记住,一致性比完美性更重要。

嗯,以上就是 RESTful API 设计的核心规范。
你可能会觉得这些规则不难,人人都懂。
但“懂”和“做”之间,差的是落地和坚持。