API 变更

REST API 版本控制:URL、标头还是查询参数,哪种合适

阅读约 1 分钟

REST API 版本控制,就是 API 告诉调用方一个请求在用哪份契约的方式,这样服务端才能改变契约,而不破坏那些还没迁移的调用方。版本可以放在三个地方:URL 路径(/v2/orders)、请求标头(Accept: application/vnd.acme.v2+json,或者自定义的 Api-Version: 2026-10-01),或者查询参数(/orders?version=2)。路径最常见,因为它在哪里都看得见;标头对 URL 最干净;查询字符串则是大多数团队事后最后悔的那一种。哪种合适,取决于你的调用方是谁,以及你打算怎样把变化告诉他们。

版本放在哪里示例谁看得见默认可缓存谁在用
URL 路径GET /v2/orders每一条日志、链接、代理和浏览器是Twitter、Shopify,大多数公开 API
自定义标头Api-Version: 2026-10-01只有设置了它的客户端需要 Vary: Api-VersionStripe(Stripe-Version)、GitHub(X-GitHub-Api-Version)
Accept 标头Accept: application/vnd.acme.v2+json只有设置了它的客户端需要 Vary: AcceptGitHub 的媒体类型,一些企业 API
查询参数GET /orders?api-version=2日志和链接,和过滤条件混在一起是,但会污染缓存键Azure REST API
不带版本GET /orders 加上只做增量式变化没有人是内部 API 和 GraphQL 风格的 API

REST API 里的 v1 和 v2 是什么?

v1 和 v2 是 API 契约两个版本的标签:同样的资源,通过不同的路径访问,而 v2 可能重命名字段、改变类型、去掉端点,或者改动 v1 调用方所依赖的错误格式。只要 v1 还在支持期内,发往 /v1/orders 的请求就一直得到旧的格式;发往 /v2/orders 的请求得到新的。这个数字是一个关于兼容性的承诺,承诺才是它有用的原因。每次发布都递增它,只会把这个标签变成噪音。

大多数 API 只有在一次变化会破坏现有调用方时,才需要一个新的主版本。新增字段、新端点或新的可选参数都可以在当前版本下发布,因为写得好的客户端会忽略自己不认识的东西。什么时候该给 API 做版本控制,以及不做的话该怎么办讲的是这个决定;本文讲的是在你决定升版本之后,REST API 版本控制里版本该放在哪里。

REST API 的版本应该放在 URL 里还是标头里?

如果你的调用方里有人会在浏览器里直接敲请求、从文档里复制示例或者读日志,就放在 URL 里;如果你希望每个资源只有一个稳定的 URL,而调用方全是 SDK 或你自己控制的代码,就放在标头里。URL 路径的明确程度无可替代:一张带着请求行的支持工单,本身就已经告诉你版本是哪个。标头必须在每个请求上设置,在链接里看不见,还很容易在代理那里丢掉,这就是为什么用标头做版本控制的 API,总会为没有发送标头的请求定义一个默认值。

除了 URL 整洁之外,标头方案还有一个实实在在的优势:它把资源的身份和资源的表示分开了,而这正是 HTTP 的设计思路。RFC 9110 描述了通过 Accept 进行的内容协商,像 application/vnd.github.v3+json 这样的媒体类型,就是以这种方式表达的版本。在实践中,设计上的纯粹性远不如调用方是否记得发送这个标头重要。

基于日期的版本控制是怎么工作的?

版本不再是 v1 和 v2,而是一个日期,每次破坏性变更都对应一个新日期。调用方固定自己接入时的那个日期,通常在账户层面设置一次,之后就一直保持那套行为,直到自己选择迁移。Stripe 的 Stripe-Version 标头和 GitHub 的 X-GitHub-Api-Version 都是这样工作的。

代价在实现上。每一次破坏性变更都变成一个小的转换层,负责在固定的日期和当前格式之间转换,而服务端要把它们叠起来:一个固定在两年前的请求,要穿过此后的每一次变化。这比再写一份处理器拷贝更重,更适合很少变化的 API。每月发一次破坏性变更的团队,最后会攒下几十个转换层。Stripe 的 API 版本控制从调用方的角度审视了这个模型,包括体验日志必须承载的内容。

为什么不推荐用查询字符串做版本控制?

因为它把契约和过滤条件混在了一起。/orders?status=open&api-version=2 让版本看起来像一个可选的细化条件,而不是决定响应里每一个字段形态的东西。它也是最容易被意外丢掉的一种,因为查询字符串会被客户端库重新拼装、被某些代理剥掉、被人手动编辑。微软自己的 REST API 设计指南记录了 Azure 使用的查询参数形式,而围绕它的那套生成式 SDK 生态,是让这种做法行得通的原因;没有那套工具,忘记参数的调用方会悄无声息地得到默认版本。

旧版本会怎么样?

它继续运行,并带着一个停止的日期。版本控制的意义在于让调用方按自己的节奏迁移,而这只有在旧版本可用的时间足够长、并且他们被告知何时结束时才成立。Google 的 API 版本控制指南描述了它为自己的 API 采用的生命周期:一个稳定版本会一直支持到公布的弃用日期,而替代版本在整个弃用窗口期内都是可用的。

公告是大多数团队投入最少的部分。一条新路径或新标头,只对那些知道它存在、知道改了什么、知道 v1 什么时候消失的调用方有用。这是三条事实,每一条都需要一个落脚处:API 体验日志里一条带日期、写到字段级差异的条目,一份写明结束日期的弃用通知,以及一份为仍在旧版本上的调用方逐项讲解每个变化的迁移指南。停用一个 API 版本讲了最后一步,包括能让客户端在每个响应上发现这个日期的 Sunset 标头。

能完全避开 REST API 版本控制吗?

能,只要每一次变化都是增量式的。只加字段,从不移除或重命名;只加端点,从不改变现有端点的返回;接受新的可选参数,从不改变旧参数的含义。很多内部 API 光靠这条规则就运行了很多年,这也是 GraphQL 模式在设计上遵循的规则。代价是一份只会增长的契约,带着一堆必须留着的已弃用字段,因为总有某个地方的某个人还在读它们。

当增量式变化做不到时,版本就是那件工具。在路径、标头和日期之间的选择,决定了调用方看到什么、变化如何公告;不变的是,变化必须写在调用方会读到的某个地方。对于有外部调用方的 API,那个地方就是体验日志,而破坏性变更一文讲了什么才算得上一次破坏性变更。

FAQ

给 REST API 做版本控制的最佳方式是什么? URL 路径里的主版本号是认知度最高、最不容易出错的方式,所以它是公开 API 的默认选择。标头版本能保持 URL 稳定,适合由 SDK 驱动的 API;基于日期的标头适合很少变化、并希望调用方固定行为的 API。

API URL 里的 v1 是什么? 它是 API 契约的第一个主版本。发往 /v1/... 的请求被承诺得到版本 1 文档所描述的格式,即使已经存在一个字段或行为不同的 /v2/。

每一次变化都需要一个新的 API 版本吗? 不需要。新增字段、端点和可选参数这类增量式变化,可以在现有版本下发布。只有会破坏现有调用方的变化才需要一个新版本。

旧的 API 版本应该支持多久? 长到足够让调用方完成迁移,对公开 API 来说,通常是替代版本可用之后的六到十二个月,日期在公告时公布,并在弃用通知里重复一次。


本文的技术内容未经独立审核。如有错误,请告诉我们,我们会更正。

changeloop 相关页面: 开发者文档, changelog 示例

changeloop
打造闭环 changelog 的团队。用户提出需求,你的团队交付,提出需求的人得知结果。