api-contract-review

Category: Coding Risk: Low risk niuwoai/skills CC-BY-4.0

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 放在请求头,服务端在一定时间窗内对同一个键返回同一个结果。没有这个,网络抖动重试就会产生重复订单、重复扣款。
  • 更新用乐观锁:请求带上 versionIf-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 用字符串传
  • 分页方式明确,排序稳定
  • 写接口支持幂等键
  • 更新有并发控制
  • 错误信息不泄露内部实现
  • 破坏性变更有版本策略和迁移路径
  • 文档有完整示例,且与实现同源