← 课程 设计好用的 API
0 / 5 单元
English
第 2 课

如何设计一个让人用得舒服的 API

API 就是一个程序提供给另一个程序的“菜单”。好的菜单一看就懂,出错时说得清楚,长列表也能处理好,而且不会突然变样。在这一课里,你会动手修复设计糟糕的请求,并看到好的版本为什么更好用。

API 设计关乎一份你要维护很多年的契约:资源建模、错误语义、在并发写入下依然可靠的分页,以及在不破坏已部署客户端的前提下演进契约。下面每个单元都需要动手操作。

第 1 单元

好好起名:API 诊所

下面每位“病人”都是一个设计糟糕的 API 请求,红色波浪线标出了问题所在。选出更健康的版本,看它被修好。

每个案例都是真实世界里常见的设计坏味道,选出更好的契约。进阶模式额外增加两个难题,在这些情况下“纯 REST”并不是正确答案。

修复前
哪个版本更好?
规则汇总
  • URL 是名词:某个东西的地址(/users/42)。方法才是动词(GET、POST、DELETE…)。
  • 列表用复数名:/orders、/orders/17。
  • GET 只能读取,绝不能修改任何东西。
  • 用路径表示“属于”(/users/42/orders),用查询参数表示筛选(?sort=price)。
  • 选定一种命名风格(小写),处处坚持。
设计检查清单
  • 围绕资源和集合建模,让 HTTP 方法承载语义(安全、幂等、可缓存)。
  • 每个拥有唯一 ID 的资源都应有一个规范的顶层 URL。嵌套只用于列表(/teams/12/members)。
  • 带副作用的状态转换可以做成显式动作:POST /orders/42/cancel,非法转换时返回 409。
  • 可组合的查询参数:filter、sort=-created_at、limit、cursor。对未知参数返回 400。
  • 契约先行(OpenAPI),并在 CI 里做 lint(例如 Spectral),让约定被强制执行,而不是靠记忆。
第 2 单元

说清楚:状态码与错误

每个响应都以一个三位数的状态码开头。程序最先读取的就是它,所以它必须正确。光看第一位数字就能知道很多:

客户端、重试、缓存、监控和告警都会根据状态码做判断。选择最具体、最正确的状态码,并返回机器可读的错误响应体(RFC 9457 问题详情)。

2xx 成功了 4xx 客户端出错了 5xx 服务器出故障了
客户端
服务器
响应
(请选择一个状态码)

糟糕的错误 vs 好的错误

✗ 难以使用
HTTP/1.1 200 OK

{ "success": false, "msg": "Error" }
  • 写着 200 OK,所以程序以为成功了。
  • “Error” 既没说哪里错了,也没说怎么改。
  • 失败时返回 2xx,会让重试、缓存、指标和告警全部失效。
  • 没有稳定的机器可读类型,客户端只能去解析字符串。
  • 没有字段级别的细节,也没有供客服排查的关联 ID。
✓ 好用
HTTP/1.1 400 Bad Request

{
  "error": "email is required",
  "field": "email"
}
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "1 field is invalid",
  "errors": [{ "pointer": "/email", "detail": "is required" }],
  "request_id": "req_7Hk2"
}
  • 光看状态码,程序就知道失败了以及失败的原因。
  • 错误信息准确指出要修改哪个字段。
  • 稳定的 type URI 让客户端可以按错误类型分支处理。
  • JSON pointer 把错误对应到表单字段;request_id 关联到你的日志。
  • 绝不要在 detail 里泄露堆栈信息或 SQL。
第 3 单元

长列表:分页

一个信息流可能有几百万条帖子,所以 API 每次只发一小部分(一“页”)。请求下一页有两种常见方式。看看在你滚动浏览时来了一条新帖子会发生什么。

在按最新优先排序、且两次翻页之间有写入的信息流上,对比 offset 分页和游标(keyset)分页。点击播放经典 bug,或者自己动手操作。

页码(offset)的意思是“跳过前 6 条,给我接下来的 3 条”。游标(cursor)就像书签:“给我 #7 之后的 3 条”。服务器数据上的彩色箭头标出了每种方式的下一页从哪里开始。
Offset:LIMIT 3 OFFSET 6。数据库要扫描并丢弃 6 行(O(offset)),而且只要上方有行被插入或删除,窗口就会移位。
游标 / keyset:WHERE (created_at, id) < (:ts, :id) ORDER BY created_at DESC, id DESC LIMIT 3。它利用索引,在写入时依然稳定。游标对外应保持不透明,例如用 base64 编码。
服务器数据 · 最新优先

页码(offset)Offset 分页

你已收到

书签(cursor)游标分页

你已收到
第 4 单元

安全地变更:是否破坏兼容?

一旦有应用在用你的 API,你就不能随便改了,因为它们的代码依赖旧的结构。判断每个变更是可以放心上线,还是会弄坏已有的应用。

假设客户端已经部署,而且你无法强制它们升级。基于这一前提给每个变更分类。进阶模式会加入那些真正引发线上事故的微妙情况。

✓ 可以放心上线

    ⚠ 破坏性变更

      黄金法则:你可以新增东西,但不要改名、删除或修改它们。如果确实必须修改,让旧版本继续工作一段时间,提前通知大家,之后再删除。
      演进契约
      • 先扩展,再收缩:新增新字段,迁移客户端,在发出 Deprecation 头并给出 Sunset 日期(RFC 8594)之后,再删除旧字段。
      • 版本管理:URL 前缀(/v2)简单直观;基于请求头或日期的版本(API-Version: 2026-10-01)允许按端点做更细粒度的变更。
      • 宽容读取者:要求客户端忽略未知字段、处理未知枚举值。这样“新增”才真正安全。
      • 在 CI 中运行契约测试(消费者驱动,例如 Pact)或 OpenAPI diff,在发布前发现破坏性变更。
      第 5 单元

      自我检测