API 契约版本控制策略
对比 URL 版本控制、请求头版本控制、基于日期的版本控制与 evolution-only。如何为 API 挑选合适的版本控制策略,以及规格文档必须就弃用时间线说清楚什么。
现实中真正存在的四种策略
我交付过或审过的每一个 REST API,用的都是四种策略之一。URL 路径版本控制把版本放进路由里,所以 /v1/customers 和 /v2/customers 是不同的资源。请求头版本控制让 URL 保持稳定,读取的是 Accept-version 或厂商自定义的媒体类型,比如 application/vnd.acme.v2+json。基于日期的版本控制是 Stripe 推起来的那一套——把客户端钉在某个日历日期上,比如 2023-10-01,把每一次破坏性变更都当成一次带日期的汇总更新。Evolution-only 则干脆拒绝做版本,永远只承诺兼容式增量演进。
除此之外都是方言。Query string 版本控制其实就是 URL 版本控制,只是缓存更糟糕。子域名版本控制会把你的 TLS 故事搞乱。要挑就挑得有意识,并把"为什么选它"写下来。
我为什么不再给 REST API 用 semver
我见过最常见的翻车,就是团队因为顺手就抓起 semver。Semver 是库和编译期消费它的用户之间的契约。线协议不是这么运作的。你升一个 minor,客户端不会被重新编译;他们会继续往你的端点上打请求,持续几个月甚至几年,直到自己决定迁移为止。
告诉大家"我们现在是 v2.4.1"传递不了任何可操作的信号。一个 minor bump 到底是加了字段、改了默认值,还是收紧了类型?不读 changelog 没人知道;而只要得读 changelog,版本号就只是装饰。我想要的是一个可以用来路由的整数,或者一个可以用来钉住的日期,而不是一个规则都说不清的三段式数字。
URL 版本控制:大多数团队的我的默认选择
对于一个面向产品、外部消费者不只是几个的 API,我默认选 URL 路径版本控制。每一个客户端工具、每一条 curl 命令、每一条代理日志,都能一眼看到版本。路由逻辑简单到不值一提,缓存不需要额外配置就能按路径做 key,新来的工程师扫一眼 access log 就能知道调用方期望什么。
坑是真的有。范围在 v1 的 auth token,如果权限模型变了,就不应该在 v2 上悄悄继续工作,所以 token introspection 必须知道版本这回事。钉在 /v1/* 上的 CDN 规则必须在 /v2/* 上再写一份。一旦 /v2 上线,所有新功能都只想落到 v2 上,于是 v1 开始腐烂——即便你承诺过支持 12 个月。要么提前预留回移关键修复的成本,要么就干脆别发 v2。
请求头版本控制:理论优雅,实践扎心
请求头版本控制在设计文档里看着很干净:URL 保持规范,版本是传输层的事,客户端自己协商要什么。实际上痛点来自"看不见的版本"。日志默认不打请求头。Curl 示例一粘出来就坏,因为没人记得加上 Accept-version 那一行。每一层缓存都得对这个请求头做 vary。一个配错的代理把它剥了,客户端就静默拿到了服务端的默认版本。
我只有在 URL 必须稳定是硬性要求时才会选请求头版本控制,这种情况通常是因为 API 是 hypermedia 驱动的。除此之外,人机工学上的税不值得交。
基于日期的版本控制:为什么 Stripe 是对的——对 Stripe 来说
Stripe 把每个账户钉在某个版本上,比如 2023-10-01。他们上一个破坏性变更时,就给它打一个新日期,你的账户会继续保持旧行为,直到你显式升级。对比一下 GitHub——他们用的是 URL 版本控制(/v3,后来 GraphQL 改成 /v4),而且极少宣布主版本升级。
这两种选择优化的是不同的东西。Stripe 在不停上破坏性变更,因为他们的领域本身在脚下移动;基于日期的版本控制让他们可以每个季度演进一次,而不必逼所有人按同一个节奏迁移。GitHub 优化的是一个巨大的第三方生态,在这里稳定好几年比这个季度能多发一次破坏性变更要值钱得多。如果你的 API 十年才变几次,URL 版本控制是老实人。如果一年要变好几次,那基于日期的版本控制能把大家从永无止境的迁移里解救出来。
代价是基础设施。你得有一层转换层,在所有支持的日期和当前内部模型之间互相翻译请求和响应。除非你准备把这一层当成永久的编制养起来,否则别选基于日期的版本控制。
Evolution-only:没人承认自己选了的那种策略
Evolution-only 意味着你白纸黑字承诺:API 只以向后兼容的方式改——新增可选字段、新增端点、新增枚举值(老客户端会忽略)。不能删,不能收紧,不能让语义漂移。大多数所谓"用 semver"的内部 API,实际上干的就是这事。
我喜欢在两种场景下用 evolution-only:一种是两端都归我管的内部 service-to-service API;另一种是公网 API 但规模小到根本不应该出现破坏性变更的情况。难的不是规则,是纪律。总会有人想改名某个字段,或者把可选字段改成必填,规格文档得给评审者明确的权力说"不"。
什么算破坏性变更
没有一个共享的定义,每一次变更都会变成一场谈判。我在用的清单:
破坏性:删字段或端点、收紧类型(integer 收到正整数、string 收到 enum)、把字段从可选改成必填、响应形状没变但语义变了、请求里加一个必填字段、改错误码、改默认值。
非破坏性:给请求加一个带合理默认值的可选字段、给响应加一个字段、加一个端点、在规格已经要求客户端容忍未知值的前提下加一个新 enum 值、加一个可选响应头。
枚举值上那个"在……的前提下"正是团队翻车的地方。如果你从没告诉过客户端要忽略未知枚举值,那再加一个就是破坏性的——哪怕它看起来只是增量。把容忍规则在你需要它之前就写进规格里。
弃用时间线和 Sunset 请求头
我的默认值:公网 API 在宣布破坏性新版本后至少给 12 个月;共享发布节奏的内部跨团队 API 给 3 到 6 个月;支付、身份这类高风险集成给 18 到 24 个月。任何少于 3 个月的都不是弃用,而是强制迁移。公告里就要老实承认这一点。
宣布下线用 RFC 8594 的 Sunset 请求头,出现在被弃用版本的每一个响应里,再配一个指向迁移文档的 Deprecation 请求头。会打响应头日志的客户端能在 deadline 咬人前好几个月就看到它。规格文档应该要求这些东西从 v2 上线那天就在,而不是 v1 下线前一周才加。
规格必须扛起的验收标准
- Given an API spec declaring URL-path versioning with v1 live When a change removes a field from the v1 response Then the review is rejected as a breaking change - Given v2 has shipped and v1 is in its deprecation window When a client sends a request to /v1/resource Then the response includes a Sunset header with the retirement date And a Deprecation header pointing to the migration guide
版本控制规格必须明说的那些事
五件事,每次都得有。第一,选定的策略,外加一段话说明它为什么匹配这个 API 的变更频率和受众。第二,一份针对你自己领域调过的破坏性变更定义。第三,带数字的弃用时间线,不是形容词。第四,客户沟通方案:弃用在哪里宣布、谁负责宣布、客户端如何以编程方式发现当前状态(Sunset 请求头、/versions 端点、status page)。第五,回滚姿态:如果 v2 证明走不通,v1 能不能重新吃流量,那个决策的截止点在哪里。
在第一条 /v1 路由存在之前,先把这五条写下来。等客户接上之后再补版本控制政策,是我见过的团队为之付出过最昂贵的返工。
可复制产物:契约评审包
当工作涉及 API 行为、schema、事件、重试或消费者预期时使用。它会把兼容性和发布证据提前摊开。
API 契约评审包:API 契约版本控制策略 本次要做的决策: - 确认契约变化是否兼容,消费者需要什么迁移动作,发布后如何观察风险。 责任人检查: - 产品责任人: - 工程责任人: - QA 或运维评审: 范围边界: - 本次包含: - 本次不包含: - 仍需确认的假设: 验收证据: - 测试或 fixture: - 日志、指标或截图: - 人工复核步骤: 契约边界:没有兼容性分类、消费者影响、重试行为和回滚说明,不进入发布。 评审追问: - 没参加需求会的人还会误解哪里? - 哪个证据能证明这次改动足够安全,可以发布?
编辑复核记录
复核日期:2026-04-28。本次补充了可复用产物,按相关主题 Hub 检查了文章定位,并收紧下一步链接,让页面更像可操作参考,而不是孤立长文。
专题阅读路径
这篇文章归入 API 契约 主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。
继续阅读
填写表单,生成完整的功能规格 Markdown——免费使用,无需注册。
编辑说明
最近复核:2026-04-28。编辑部检查了示例、内链和可复制评审片段,确保内容更适合真实项目使用。
本文面向软件交付团队,介绍API 契约版本控制策略。示例均为工程场景说明,不构成法律、税务或投资建议。
- 作者信息:Daniel Marsh
- 编辑政策:文章审阅与更新方式
- 纠错:联系编辑