API 版本管理
后端改接口时,最容易忽略的是:你看不到所有调用方。
- 网页可以随时发布新版本,但用户手机上的旧版 App 可能好几个月都不会更新;
- 第三方合作方按照你半年前的文档写了对接代码,不会因为你改了接口就跟着改;
- 同事维护的另一个服务,可能也在调用这个接口。
所以“改个字段名”这种在代码里看起来很小的改动,上线后可能导致一批旧版 App 直接崩溃。版本管理要解决的就是:怎样让接口持续演进,同时不弄坏已有的调用方。
读完这篇你会
- 判断一个改动是否会破坏兼容
- 用“先扩展、再迁移、最后收缩”的方式安全地修改字段
- 选择合适的版本策略,并让废弃和下线有据可依
1. 原理:接口是一份合同
可以把接口想成墙上的插座标准:你可以新增一种插孔,但不能把现有插孔的形状改了,否则所有按旧标准生产的插头都插不进去。
判断改动是否安全,只需要问一句:按旧约定写的客户端,在新接口下还能正常工作吗?
| 改动 | 是否破坏兼容 |
|---|---|
| 新增一个可选的响应字段 | 一般不会(前提是客户端忽略不认识的字段) |
| 新增一个可选的请求参数 | 不会 |
| 新增一个接口 | 不会 |
| 放宽校验(以前拒绝的现在接受) | 不会 |
| 删除或重命名字段 | 会 |
| 修改字段类型(字符串改成数字) | 会 |
| 修改字段含义(金额从“元”改成“分”) | 会,而且最隐蔽:不报错,只是数据全错 |
| 收紧校验(以前接受的现在拒绝) | 会 |
| 新增枚举值 | 可能会,取决于客户端怎么处理未知值 |
一句话记住:新增通常是便宜的,删除和改含义是昂贵的。
2. 坑一:改了一个字段名,旧版 App 崩溃
现象:后端把用户信息里的 nickName 统一改成 nickname,网页端同步更新了,一切正常。上线后,崩溃监控显示大量来自旧版 App 的崩溃:读取 nickName 得到空值,后续代码没有判空。
原因:网页端可以和后端同时发布,但已经安装在用户手机上的旧版 App 不会自动更新,它们仍然按旧字段名读取数据。
修复:不要一步到位地改名,而是分三步走,这个做法叫“扩展—迁移—收缩”:
text
第 1 步 扩展:同时返回新旧两个字段
{ "nickName": "小林", "nickname": "小林" }
第 2 步 迁移:新版客户端改用 nickname;观察旧字段的使用量,
等使用旧字段的客户端版本占比降到可以接受的程度
第 3 步 收缩:在新的接口版本中移除 nickName怎么避免再犯:请求日志里记录客户端类型和版本号(比如通过 X-App-Version 请求头)。这样删除字段前,你能用数据回答“还有多少旧版本在使用它”,而不是靠猜。
3. 坑二:新增一个枚举值,老客户端出错
现象:订单状态原来只有 pending、paid、shipped 三种,后端新增了 refunding(退款中)。老版本客户端遇到这个值,有的页面直接白屏,有的显示成了空白状态。
原因:客户端代码大概是这样写的:
js
const STATUS_TEXT = { pending: '待支付', paid: '已支付', shipped: '已发货' }
statusLabel.textContent = STATUS_TEXT[order.status].toUpperCase() // refunding → undefined,报错修复:两边都要做:
- 客户端:任何枚举都要有“未知值”的兜底分支,显示一个合理的默认文案,而不是报错;
- 服务端:新增枚举值时,可以根据客户端版本判断,对不支持它的旧客户端返回一个它能理解的近似值,或者把新值放在新字段里。
js
statusLabel.textContent = STATUS_TEXT[order.status] ?? '处理中'怎么避免再犯:在接口约定里写明“客户端必须容忍未知的枚举值”,并在代码评审中检查。
4. 坑三:收紧了校验,合作方的对接全部失败
现象:为了数据质量,后端把“备注”字段的最大长度从 500 改成了 100。上线当天,一家合作方的批量下单全部失败,因为他们的系统会自动生成很长的备注。
原因:以前被接受的请求,现在被拒绝了。这对调用方来说就是破坏性变更。
修复:收紧规则要分阶段:
- 先只记录违反新规则的请求(不拒绝),统计影响了哪些调用方;
- 通知这些调用方修改,给出明确的截止时间;
- 到期后再真正开始拒绝。
5. 坑四:只是新增了一个字段,客户端也崩了
现象:响应里新增了一个字段,按理说完全兼容,但某个用强类型语言写的客户端反序列化时报错。
原因:这个客户端被配置为“遇到不认识的字段就报错”。
修复:和所有调用方约定宽容的读取方式:遇到不认识的字段直接忽略。这是接口能够持续扩展的前提。反过来,服务端在接收请求时也要想清楚:多余的字段是忽略还是拒绝,并保持一致。
6. 什么时候需要新版本,版本号放在哪里
只有破坏性变更无法通过“扩展—迁移—收缩”平滑完成时(比如整个资源结构都要重新设计),才需要开一个新的接口版本。
| 方式 | 形式 | 特点 |
|---|---|---|
| URL 路径 | /v1/users、/v2/users | 一眼就能看出版本,调试、文档、监控都方便 |
| 请求头 | Accept: application/vnd.example.v2+json | URL 干净,但调试时容易忘带,部分代理和缓存处理不好 |
| 查询参数 | /users?version=2 | 改动小,但容易被遗漏,缓存语义也不清晰 |
对大多数业务接口,URL 路径版本最务实。
7. 废弃和下线:先通知,再观察,最后移除
text
1. 标记废弃:文档里注明,响应头里提示
2. 观察使用:按客户端统计还有谁在用旧接口或旧字段
3. 通知迁移:给出明确的下线时间
4. 正式移除:使用量降到可接受的水平后再下线可以在响应头里告诉调用方:这个接口已经废弃、什么时候下线、新接口在哪里。
http
Deprecation: @1782864000
Sunset: Thu, 31 Dec 2026 23:59:59 GMT
Link: </v2/users>; rel="successor-version"Deprecation 表示从什么时候开始废弃(@ 后面是 Unix 时间戳),Sunset 表示计划下线的时间。
同时维护的版本越多,每个 bug 修复、每次测试的成本就越高。一般只保留“当前稳定版”和“正在迁移的旧版”两个;如果同时存在三个以上的版本,通常说明废弃流程没有执行到位。
排查思路
- 上线后某类客户端出错:按客户端类型和版本号筛选错误日志和崩溃报告,确认是否集中在旧版本。
- 对比新旧响应:保存改动前后同一请求的响应,逐字段对比(可以用 JSON diff 工具),找出被删除、改名、改类型的字段。
- 准备下线旧接口:先查访问日志里最近一段时间还有哪些调用方、调用量多大。
- 契约测试:为关键接口编写自动化测试,校验字段名、类型和必填性,接口发生破坏性变化时在合并前就能发现,见 API 契约与前后端联调。
检查清单
- [ ] 每次接口改动都判断过是否破坏兼容
- [ ] 改名、改类型通过“扩展—迁移—收缩”完成,不一步到位
- [ ] 客户端容忍未知字段和未知枚举值
- [ ] 收紧校验先观察、再通知、最后执行
- [ ] 请求日志记录了客户端类型和版本号
- [ ] 废弃的接口和字段有明确的下线时间,同时维护的版本不超过两个
延伸阅读
- REST API 设计:接口的基本约定
- API 契约与前后端联调:用契约测试防止意外破坏
- PC、H5 与小程序 API:多端共用接口时的兼容问题