api-contract-review
name: api-contract-review
description: HTTP API 接口设计评审与前后端联调约定。当要设计一组新接口、要审接口文档、前后端对不上、错误码混乱、分页和时间格式不统一、或者要做 API 版本演进时使用。触发词:接口设计、API 设计、接口文档、联调、错误码、状态码、分页、幂等、RESTful、接口版本、契约、OpenAPI、字段命名。不负责数据库设计(走 db-migration-review)和网关路由(走 llm-gateway-selection)。
API 契约评审
前后端联调扯皮的根源基本都是契约没定清楚:错误怎么表达、分页怎么翻、时间什么格式、空值用什么。这几件事在写第一行代码前定下来,能省掉后面反复的返工。
一、状态码
用 HTTP 状态码表达这次请求发生了什么,不要一律返回 200 再在 body 里写 code。后者会让所有中间件(网关、监控、重试、CDN)都失去判断能力。
| 码 | 用于 |
|---|---|
| 200 | 成功且有内容 |
| 201 | 创建成功,Location 头指向新资源 |
| 204 | 成功且无内容(删除、无返回的更新) |
| 400 | 请求本身有问题(参数缺失、格式错) |
| 401 | 没有身份或身份失效 |
| 403 | 有身份但没权限 |
| 404 | 资源不存在 |
| 409 | 状态冲突(重复创建、并发修改) |
| 422 | 格式对但业务校验不过 |
| 429 | 触发限流,必须带 Retry-After |
| 500 | 服务端自己的问题 |
| 502/503/504 | 依赖故障,客户端可重试 |
401 和 403 分清楚:401 让客户端去重新登录,403 让它别再试了。混用会导致客户端陷入无限刷新登录态的循环。
二、错误体
统一一种结构,所有接口一致:
{
"error": {
"code": "quota_exceeded",
"message": "本月额度已用完",
"details": { "limit": 100000, "used": 100000, "reset_at": "2026-10-01T00:00:00Z" },
"trace_id": "01JB8N2K7Q"
}
}
规矩:
code是给程序看的,小写下划线,稳定不变。客户端按它做分支判断。message是给人看的,可以改,可以本地化。客户端不许拿 message 做判断。trace_id必须有,且和日志能对上。用户截图报错时,这一个字段能省掉半小时排查。details放结构化的补充信息,别把关键信息只写在 message 里。- 不要在错误里泄露内部细节:SQL 语句、堆栈、内部主机名、文件路径。这些进日志,不进响应。
错误码要有一份清单文档,新增错误码走评审。散落在代码里的字符串错误码,半年后就没人知道有哪些了。
三、字段约定
一次定死,全局统一:
| 项 | 约定 |
|---|---|
| 命名风格 | 全 snake_case 或全 camelCase,不混用 |
| 时间 | RFC 3339 带时区的字符串,如 2026-09-03T10:42:00+08:00 |
| 时间戳 | 需要数字时明确单位,字段名带 _ms 或 _at_unix |
| 金额 | 整数最小单位(分),或字符串定点数。绝不用浮点 |
| 布尔 | true / false,不要 0 / 1 / "Y" |
| 空值 | 明确「不存在」用 null 还是省略字段,二选一贯彻到底 |
| 枚举 | 小写下划线字符串,不用数字。数字枚举在日志里读不懂 |
| ID | 字符串。数字 ID 在 JavaScript 里超过 2^53 会精度丢失 |
最后一条踩过的人特别多。 后端用 int64 主键,前端 JSON.parse 之后 ID 悄悄变了,排查起来极其痛苦。ID 一律用字符串传。
四、分页
两种,按场景选:
页码分页:适合需要跳页的后台列表。
GET /items?page=2&page_size=20
→ { "items": [...], "page": 2, "page_size": 20, "total": 1043 }
代价是深翻页很慢,且翻页过程中有新数据插入会导致重复或遗漏。
游标分页:适合信息流和大数据集,也是默认推荐。
GET /items?limit=20&cursor=eyJpZCI6MTAwfQ
→ { "items": [...], "next_cursor": "eyJpZCI6ODB9", "has_more": true }
- 游标要不透明(编码过的字符串),客户端不许解析和构造。
has_more要显式给,不要让客户端靠「返回数量小于 limit」来猜。total在大表上很贵,能不给就不给,或者给一个明确标注的估算值。
无论哪种,排序必须稳定。按 created_at 排序时如果有并列值,要加主键做次级排序,否则翻页会重复。
五、幂等与并发
- 所有写接口都该支持幂等键。客户端生成一个
Idempotency-Key放在请求头,服务端在一定时间窗内对同一个键返回同一个结果。没有这个,网络抖动重试就会产生重复订单、重复扣款。 - 更新用乐观锁:请求带上
version或If-Match: <etag>,不匹配返回 409。避免后提交的覆盖先提交的。 - 删除要幂等:删一个已经不存在的资源,返回 204 而不是 404。
六、版本演进
- 版本放在路径里(
/v1/...)最简单直观,放在请求头里更「纯粹」但排查困难。优先路径。 - 向后兼容的变更(加字段、加可选参数、加枚举值)不需要升版本。
- 破坏性变更(删字段、改字段含义、改类型、必填参数)必须升版本或走三步迁移:新旧并存 → 通知迁移 → 下线旧的。
- 客户端必须容忍未知字段。 后端加字段不应该让老客户端崩溃。这条要写进客户端的编码规范。
- 老版本下线要有公告期和监控(看还有多少流量在用),不能说停就停。
七、其他必须约定的
- 鉴权方式:Bearer token 放
Authorization头。token 不要放 URL 查询参数,会进日志和 Referer。 - 限流:响应头返回
X-RateLimit-Limit/X-RateLimit-Remaining/X-RateLimit-Reset,429 时带Retry-After。 - CORS:明确允许的源,不要图省事写
*,尤其是带凭证的请求。 - 请求体大小上限,超了返回 413。
- 批量接口要定义部分失败的语义:全成功才算成功,还是逐条返回结果。含糊不清的批量接口是事故温床。
- 长耗时操作改成异步:立即返回 202 和一个任务 ID,客户端轮询或走回调。
八、文档与验证
- 接口文档用 OpenAPI 描述,从代码生成或用它生成代码,避免文档和实现分叉。手写的文档一定会过期。
- 每个接口至少给一个完整的请求和响应示例,包含错误情况的示例。
- 契约测试:前后端各自对着同一份 schema 跑测试,联调前就能发现不一致。
- 接口变更走 PR,让前端能 review。
九、评审清单
- 状态码语义正确,没有「一律 200」
- 错误结构统一,有稳定的 code 和 trace_id
- 命名风格、时间格式、金额类型、ID 类型全局一致
- ID 用字符串传
- 分页方式明确,排序稳定
- 写接口支持幂等键
- 更新有并发控制
- 错误信息不泄露内部实现
- 破坏性变更有版本策略和迁移路径
- 文档有完整示例,且与实现同源