Skip to content

REST API 设计 ​

接口设计得好不好,调用方最有发言权。设计不好的接口,会让前端、移动端、第三方对接的同学反复踩坑:

  • 一个“删除”接口用的是 GET,某天聊天软件生成链接预览时,顺手把数据删了;
  • 用 PUT 只更新了昵称,结果用户的头像和简介被清空了;
  • 所有接口都返回 HTTP 200,监控系统一片绿,用户却在大量报错;
  • 列表接口不分页,数据量涨上来后,一次返回好几 MB,接口频繁超时。

REST 的核心思路很简单:URL 表示资源,HTTP 方法表示动作,状态码表示结果。遵守这套约定,调用方就不用为每个接口学一套新规则。

读完这篇你会

  • 用名词设计资源 URL,用 HTTP 方法表达意图
  • 理解“安全”和“幂等”,知道它们为什么影响重试和缓存
  • 设计一致的状态码、错误体和分页

1. 原理:资源 + 统一的操作方式 ​

可以把 REST 想成一座图书馆:每本书都有一个固定的编号(URL),借、还、查询都是统一的手续(HTTP 方法)。你不需要为每本书单独学一套借阅流程。

text
/articles            文章集合
/articles/18         编号 18 的文章
/users/42/articles   用户 42 的文章

URL 用名词表示“是什么”,不要用动词表示“做什么”:

text
✅ GET    /articles/18
❌ GET    /getArticle?id=18
❌ POST   /deleteArticle

筛选、排序、分页放在查询参数里:

text
GET /articles?authorId=42&status=published&sort=-createdAt&page=2&pageSize=20

2. HTTP 方法表达意图 ​

动作方法例子成功时
查询列表GETGET /articles200
查询单个GETGET /articles/18200,找不到返回 404
创建POSTPOST /articles201,并返回新资源
整体替换PUTPUT /articles/18200 或 204
部分修改PATCHPATCH /articles/18200
删除DELETEDELETE /articles/18204

安全和幂等:两个决定“能不能放心重试”的性质 ​

方法安全(不改变服务器数据)幂等(执行一次和执行多次,结果一样)
GET / HEAD是是
PUT否是:把文章设成某个内容,设几次都一样
DELETE否是:删一次和删三次,结果都是“已删除”
POST否否:提交三次就可能创建三个订单
PATCH否不一定,取决于具体操作(“设为 5”幂等,“加 1”不幂等)

这两个性质不是理论,它们直接决定了基础设施的行为:浏览器、代理、爬虫会认为 GET 是安全的,随时可能主动发起;网络超时后,客户端和网关会认为幂等的请求可以放心重试。

3. 坑一:用 GET 做删除,数据被“自动”删了 ​

现象:管理后台有一个链接 GET /admin/posts/18/delete。某天有人把后台页面链接发到了聊天群里,几篇文章莫名其妙被删除了,操作日志里的访问者是一个链接预览服务。

原因:GET 被约定为“安全”的,于是很多程序会主动访问 GET 链接:聊天软件生成链接预览、浏览器预加载、搜索引擎爬虫、安全扫描工具。它们都不会想到“访问一下就会删数据”。

修复:所有修改数据的操作都用 POST、PUT、PATCH 或 DELETE。

text
DELETE /posts/18

4. 坑二:用 PUT 只传了昵称,头像和简介被清空了 ​

现象:前端只想修改昵称,发送 PUT /users/42 { "nickname": "小林" }。请求成功了,但用户的头像、简介都变成了空的。

原因:PUT 的语义是用请求体整体替换资源。请求体里没有的字段,按语义就应该被清空或设为默认值,后端也确实这么实现了。

修复:

  • 只修改部分字段,用 PATCH,后端只更新请求里出现的字段;
  • 坚持用 PUT 的话,前端必须提交完整的资源。
http
PATCH /users/42
Content-Type: application/json

{ "nickname": "小林" }

实现 PATCH 时注意区分“没传这个字段”(不修改)和“传了 null”(清空),两者含义不同。

5. 坑三:所有接口都返回 200 ​

现象:系统大面积报错时,监控面板上 HTTP 成功率依然是 100%;网关的自动重试、CDN 的缓存规则也都“认为一切正常”。

原因:所有响应都是 HTTP 200,真正的结果藏在响应体的 code 字段里。所有只看 HTTP 状态码的工具——监控、日志分析、网关、缓存、浏览器——都无法分辨成功和失败。

修复:用 HTTP 状态码表达结果的大类,响应体里的 code 表达具体原因。如果历史原因必须保留“统一包装 + 业务码”的格式,至少让失败请求返回对应的 4xx、5xx 状态码。

状态码含义客户端通常怎么做
200 / 201 / 204成功 / 已创建 / 成功但无内容正常处理
400请求格式错误提示检查输入
401未登录或登录过期刷新登录态或跳转登录
403已登录,但没有权限提示无权限
404资源不存在提示已删除或链接无效
409与当前状态冲突(如重复创建、版本冲突)提示刷新后重试
422内容不符合业务规则把错误显示在对应字段上
429请求太频繁稍后重试
500 / 502 / 503服务端出错 / 网关错误 / 暂时不可用提示稍后重试,记录请求 ID

错误响应体保持统一的结构:

json
{
  "code": "VALIDATION_ERROR",
  "message": "请检查提交内容",
  "fields": { "title": "标题不能为空" },
  "requestId": "req_7f3a9c"
}

code 给程序判断,message 给用户看,fields 用来定位表单字段,requestId 用来查日志。

6. 坑四:列表不分页,越来越慢 ​

现象:上线时只有几十条数据,接口很快。一年后数据涨到几万条,列表接口每次返回好几 MB,经常超时,数据库也被拖慢。

修复:所有列表接口从第一天起就分页,并限制每页的最大数量:

json
{
  "items": [],
  "page": 2,
  "pageSize": 20,
  "total": 1234
}

翻页很深(比如第 5000 页)时,OFFSET 分页会越来越慢,因为数据库要先跳过前面所有的行。数据量大、只需要“下一页”的场景(如信息流),改用游标分页:

text
GET /articles?limit=20&after=eyJpZCI6MTgwfQ   ← after 是上一页最后一条的位置标记

7. 坑五:403 还是 404? ​

用户请求一个存在但无权访问的资源时,一般返回 403。但对于“连它是否存在都不应该让对方知道”的私密资源(比如别人的私有仓库、未公开的订单),返回 403 等于告诉对方“这个编号的东西确实存在”,攻击者可以借此枚举数据。这类场景返回 404 更安全。

团队里约定好规则,同一类资源保持一致即可。

8. 不是所有操作都适合 CRUD ​

“发布文章”“取消订单”“重置密码”这类业务动作,有明确的副作用和规则,硬塞进 PATCH status 会让权限和校验变得混乱。可以把动作建模为子资源:

text
POST /articles/18/publish
POST /orders/9527/cancellation
POST /password-reset-requests

形式可以讨论,关键是团队内保持一致,并且让 URL、方法、权限、幂等性共同清楚地表达业务含义。

排查思路 ​

  1. 用 curl -i 看完整响应:状态码、响应头和响应体一起看,而不是只看响应体。
  2. 数据被意外修改或删除:检查访问日志里的请求方法和来源,看是否有 GET 请求触发了写操作。
  3. 字段被清空:确认调用方用的是 PUT 还是 PATCH,以及请求体里缺了哪些字段。
  4. 接口变慢:看返回数据的大小和数量,以及数据库里对应查询的执行计划。
  5. 维护一份 OpenAPI 文档,接口的路径、参数、状态码和错误格式都写清楚,前后端按文档联调。

检查清单 ​

  • [ ] URL 用名词表示资源,筛选分页放在查询参数中
  • [ ] 所有修改数据的操作都不使用 GET
  • [ ] 部分修改用 PATCH,PUT 要求完整资源
  • [ ] 失败返回对应的 4xx / 5xx 状态码,错误体结构统一
  • [ ] 列表接口有分页和最大页大小,大数据量考虑游标分页
  • [ ] POST 等非幂等接口考虑重复提交的防护
  • [ ] 有维护中的接口文档

延伸阅读 ​

为复用而记录,为理解而整理。