REST API 设计 是做什么的?
API 就是产品:消费者的心智模型决定了 API 的形状,而不是你的数据库 schema。此技能要预防的高代价错误是交付一个不一致的 API 面——混合的命名、错误的状态码、在增长的表上使用偏移分页——这些错误会被每个消费者硬编码适配,一旦集成就几乎无法修复。
操作流程
按此顺序进行。资源优先于方法,因为方法的语义取决于资源是什么;错误和分页契约在编写规范之前确定,因为事后修补会破坏消费者。
步骤 1:收集输入
在设计之前收集信息。任何假设都要标记为猜测。
- 消费者:内部服务、第一方客户端或外部开发者。外部消费者会提高一致性标准——如未知则默认按外部处理。
- 领域的核心名词以及每个名词的归属关系(驱动嵌套层次)。
- 预期的集合大小和增长情况。可能超过 ~10,000 行的列表应排除偏移分页。
- 每个端点的读写比例和延迟期望。
- 认证模型(影响 401 和 403 的正确性)。
步骤 2:命名资源
- 复数名词:/users、/orders、/products。
- 嵌套一层表示归属关系:/users/{id}/orders。不要嵌套超过两层——改用过滤器展平(/orders?user_id=... 优于 /users/{id}/orders/{oid}/items/{iid})。
- 路径中不要使用动词;HTTP 方法就是动词。对于没有对应 CRUD 动词的真正操作(例如发送发票),可以建模为一个子资源 POST:POST /invoices/{id}/send-attempts。
- 大小写一致:多词路径段使用 kebab-case,JSON 字段使用 snake_case——选定一种,全局应用。
步骤 3:分配方法和状态码
- GET:读取,幂等,无请求体。返回 200 并附带体。
- POST:创建。返回 201,并在 Location 头部指向新资源。在响应体中返回创建的资源表示,这样客户端可以省略后续的 GET 请求。
- PUT:完全替换,幂等。缺失的字段意味着“清除”,而不是“保留”。
- PATCH:部分更新(JSON Merge Patch 或 JSON Patch——在规范中说明采用哪一种)。
- DELETE:删除,幂等,返回 204。删除一个已删除的资源返回 204 或 404——选择其中一种并记录在文档中。
- 错误:400 表示格式错误的输入,401 表示缺失/无效的凭据,403 表示有效凭单但无权限,404 表示资源不存在(当暴露存在性本身是一种泄露时,也应使用 404 而非 403),409 表示状态冲突,422 表示格式正确但语义无效的输入,429 表示速率限制(附带 Retry-After 头部),500 表示服务器故障——永远不要暴露堆栈跟踪或内部标识符。
步骤 4:确定错误契约
所有错误使用统一格式,机器可读的错误码优先:
{
"error": "validation_error",
"message": "Email is required",
"field": "email",
"request_id": "req_abc123"
}
request_id 必须在服务端记录,以便支持人员关联问题。Human message 专为开发人员阅读日志而设,永远不要用于最终用户显示。
步骤 5:确定分页和过滤
- 对任何会增长的集合使用基于游标的分页:返回一个不透明的游标和 has_more 标志。偏移分页仅适用于小范围、有界、面向管理员的列表——它在并发写入下线性退化并导致跳行/重复行。
- 默认页面大小为 25,最大为 100。超过最大值的页面大小应返回 400 拒绝,不要静默截断。
- 过滤和排序通过查询参数进行:?status=active&sort=created_at:desc。记录哪些字段可过滤;对未知过滤参数返回 400 拒绝,而不是忽略它们(静默忽略会隐藏客户端错误)。
步骤 6:应用性能和负载预算
- p95 延迟预算:交互式端点 300 毫秒,报告/导出 1 秒。如果某个操作无法在 1 秒内完成,则将其改为异步:返回 202 Accepted 并附带一个状态资源供轮询。
- 响应负载预算:保持典型响应在 100 KB 以下;任何经常超过 1 MB 的响应都需要分页、字段选择 (?fields=) 或专门的导出流程。
- 在 URL 路径中标识版本变更 (/v1/)。新增字段等兼容性变更不需要版本号递增。弃用时间线的处理请交给 api-versioning-strategist。
对比示例
糟糕:
POST /getUserOrders?userId=42&page=3
→ 200 { "data": [...], "error": null }
路径中包含动词,对 POST 使用了 GET 语义,采用了偏移分页,200 响应中包裹了一个 error 字段,导致客户端必须解析响应体才能知道调用是否失败。
良好:
GET /users/42/orders?status=shipped&limit=25&cursor=eyJpZCI6OTh9
→ 200 { "orders": [...], "next_cursor": "eyJpZCI6MTIzfQ", "has_more": true }
→ 404 { "error": "not_found", "message": "User 42 does not exist", "request_id": "req_9f2" }
名词路径,归属嵌套,游标分页,状态码直接表明结果。
交付物
生成一个端点规范,其中包含:资源表格(路径、方法、每个方法涉及的状态码)、共享的错误格式、分页契约(游标格式、默认值、最大值)、过滤白名单,以及每个端点的延迟/负载预算。
禁止事项
- 不要在 API 中直接映射数据库 schema——消费者会与你想要更改的内部实现耦合。
- 不要为失败操作返回 200 并附带错误体;客户端和监控工具都依赖状态码。
- 不要对无界集合使用偏移分页——它会在写入下重复和跳过行,并且线性退化。
- 不要嵌套路径超过两层;深度嵌套只编码了一种访问路径,排除了所有其他路径。
- 不要静默忽略未知的查询参数——输入错误的过滤器却返回未过滤的数据是一种数据泄露级别的错误。
质量标准
- 消费者可以根据 spec 实现客户端,无需提出任何问题。
- 每个端点都明确其状态码,包括错误情况。
- 所有集合端点共享一个分页契约;所有错误共享一种格式。
- 相同概念在每一个路径和负载中都有相同的名称。
- 每个端点都有明确的延迟预算,超过 1 秒的操作是异步的。