关于休息:API版本控制的最佳实践?

Best practices for API versioning?

对于Web服务RESTAPI版本控制,有什么已知的方法或最佳实践吗?

我注意到AWS通过端点的URL进行版本控制。这是实现同一目标的唯一方法还是其他方法?如果有多种方法,每种方法的优点是什么?


这是一个好问题,也是一个棘手的问题。URI设计的主题同时也是RESTAPI最突出的部分,因此,它可能是对该API用户的长期承诺。好的。

由于应用程序的发展,以及在较小程度上,其API是一个生命的事实,而且它甚至类似于一个看似复杂的产品(如编程语言)的发展,因此URI设计应该具有较少的自然约束,并且应该随着时间的推移而保留。应用程序和API的寿命越长,对应用程序和API用户的承诺就越大。好的。

另一方面,生活的另一个事实是,很难预见API将消耗的所有资源及其方面。幸运的是,没有必要设计整个API,直到天启。正确定义每个资源和资源实例的所有资源端点和寻址方案就足够了。好的。

随着时间的推移,您可能需要向每个特定资源添加新的资源和新的属性,但是一旦资源寻址方案成为公共方案(因此是最终方案),API用户访问特定资源所遵循的方法不应改变。好的。

此方法适用于早期API版本中支持的HTTP动词语义(例如Put应始终更新/替换)和HTTP状态代码(它们应继续工作,以便在没有人为干预的情况下工作的API客户端能够继续这样工作)。好的。

此外,由于将API版本嵌入到URI中会破坏超媒体作为应用程序状态引擎的概念(如Roy T.Fieldings博士论文所述),因为它的资源地址/URI会随着时间的推移而变化,因此我认为API版本不应长期保存在资源URI中,这意味着该资源API用户可以依赖的URI应该是permalink。好的。

当然,可以在基URI中嵌入API版本,但只能用于合理和受限的用途,如调试和新API版本一起工作的API客户端。这种版本化的API应该是有时间限制的,并且只能用于有限的API用户组(如在封闭的beta期间)。否则,你就把自己放在不该放的地方。好的。

关于API版本维护的一些想法,它们都有到期日期。通常用于实现Web服务的所有编程平台/语言(Java、.NET、PHP、Perl、Rails等)允许Web服务端点(S)与基础URI的容易绑定。通过这种方式,可以很容易地收集和保持文件/类/方法的集合在不同的API版本之间分开。好的。

从api用户pov来看,当某个特定的api版本如此明显,但仅限于有限的时间(即开发期间)时,使用它并绑定到该版本也更容易。好的。

从API维护人员的POV来看,通过使用主要处理文件的源代码控制系统作为(源代码)版本控制的最小单元,可以更容易地并行维护不同的API版本。好的。

但是,对于在URI中清晰可见的API版本,有一个警告:人们也可能反对这种方法,因为在URI设计中,API历史变得可见/并行,因此随着时间的推移,很容易发生变化,这违背了REST的准则。我同意!好的。

解决这个合理的异议的方法是在无版本API基URI下实现最新的API版本。在这种情况下,API客户端开发人员可以选择:好的。

  • 针对最新的API进行开发(致力于维护应用程序,以保护其不受可能破坏其糟糕的API客户端的最终API更改的影响)。好的。

  • 绑定到特定版本的API(很明显),但时间有限好的。

例如,如果api v3.0是最新的api版本,则以下两个应该是别名(即行为与所有api请求相同):好的。

1
2
3
http://shonzilla/api/customers/1234
http://shonzilla/api/v3.0/customers/1234
http://shonzilla/api/v3/customers/1234

此外,如果仍试图指向旧API的API版本已过时或不再受支持,则应通知仍尝试指向旧API的API客户端使用最新的早期API版本。所以访问任何过时的URI,比如:好的。

1
2
3
4
5
http://shonzilla/api/v2.2/customers/1234
http://shonzilla/api/v2.0/customers/1234
http://shonzilla/api/v2/customers/1234
http://shonzilla/api/v1.1/customers/1234
http://shonzilla/api/v1/customers/1234

应返回指示重定向的30x HTTP状态代码中的任何一个,该重定向与LocationHTTP头一起使用,该头重定向到资源URI的适当版本,该版本仍为该版本:好的。

1
http://shonzilla/api/customers/1234

至少有两个重定向HTTP状态代码适用于API版本控制方案:好的。

  • 301已永久移动,表示具有请求的URI的资源将永久移动到另一个URI(该URI应该是不包含API版本信息的资源实例permalink)。此状态代码可用于指示过时/不受支持的API版本,通知API客户端版本化的资源URI已被资源permalink替换。好的。

  • 302发现,指示请求的资源临时位于其他位置,而请求的URI可能仍然受支持。当无版本的URI暂时不可用,并且应使用重定向地址(例如,指向嵌入API版本的URI)重复请求,并且我们希望告诉客户端继续使用它(即permalinks)时,此状态代码可能很有用。好的。

  • 其他场景可以在HTTP 1.1规范的重定向3xx章节中找到。好的。

好啊。


URL不应包含版本。该版本与您请求的资源的"想法"无关。您应该试着将URL看作是您想要的概念的路径,而不是您想要返回项目的方式。版本指示对象的表示,而不是对象的概念。正如其他海报所说,您应该在请求头中指定格式(包括版本)。

如果您查看具有版本的URL的完整HTTP请求,则如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
(BAD WAY TO DO IT):

http://company.com/api/v3.0/customer/123
====>
GET v3.0/customer/123 HTTP/1.1
Accept: application/xml

<====
HTTP/1.1 200 OK
Content-Type: application/xml
<customer version="3.0">
  <name>Neil Armstrong</name>
</customer>

头部包含一行,其中包含您请求的表示形式("accept:application/xml")。这就是版本应该去的地方。每个人似乎都在掩盖这样一个事实:你可能需要不同格式的相同的东西,客户应该能够要求它想要什么。在上面的示例中,客户机要求对资源进行任何XML表示,而不是真正表示它想要什么。理论上,服务器可以返回与请求完全无关的内容,只要它是XML,并且必须对其进行解析以认识到它是错误的。

更好的方法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
(GOOD WAY TO DO IT)

http://company.com/api/customer/123
===>
GET /customer/123 HTTP/1.1
Accept: application/vnd.company.myapp.customer-v3+xml

<===
HTTP/1.1 200 OK
Content-Type: application/vnd.company.myapp-v3+xml
<customer>
  <name>Neil Armstrong</name>
</customer>

此外,假设客户机认为XML过于冗长,现在他们需要的是JSON。在其他示例中,您必须为同一个客户提供一个新的URL,这样您最终将得到:

1
2
3
4
(BAD)
http://company.com/api/JSONv3.0/customers/123
  or
http://company.com/api/v3.0/customers/123?format="JSON"

(或类似的东西)。实际上,每个HTTP请求都包含您要查找的格式:

1
2
3
4
5
6
7
8
9
10
11
12
(GOOD WAY TO DO IT)
===>
GET /customer/123 HTTP/1.1
Accept: application/vnd.company.myapp.customer-v3+json

<===
HTTP/1.1 200 OK
Content-Type: application/vnd.company.myapp-v3+json

{"customer":
  {"name":"Neil Armstrong"}
}

使用这种方法,您在设计上有了更多的自由,并且实际上坚持了休息的原始想法。您可以在不中断客户机的情况下更改版本,也可以在更改API时逐步更改客户机。如果选择停止支持表示,则可以使用HTTP状态代码或自定义代码响应请求。客户机还可以验证响应的格式是否正确,并验证XML。

还有很多其他的优点,我在我的博客上讨论其中的一些:http://thereinsorightway.blogspot.com/2011/02/versioning-and-types-in-resthttp-api.html

最后一个示例说明如何将版本放入URL中是错误的。假设您想要在对象内部获得一些信息,并且您已经对各种对象进行了版本控制(客户是3.0版,订单是2.0版,ShipTo对象是4.2版)。这是您必须在客户机中提供的恶意URL:

1
2
(Another reason why version in the URL sucks)
http://company.com/api/v3.0/customer/123/v2.0/orders/4321/


我们发现把这个版本放在URL中既实用又有用。它使您很容易一目了然地知道您在使用什么。我们使用alias/foo to/foo/(最新版本),以方便使用、缩短/清理URL等,正如公认的答案所建议的那样。

永远保持向后兼容性通常成本高昂和/或非常困难。我们更愿意提前通知取消预测、这里建议的重定向、文档和其他机制。


我同意对资源表示形式进行版本控制更好地遵循REST方法……但是,自定义mime类型(或附加版本参数的mime类型)的一个大问题是,对HTML和javascript中的write-to-accept和content-type头的支持较差。

例如,IMO不可能在HTML5表单中使用以下标题发布,以便创建资源:

1
2
Accept: application/vnd.company.myapp-v3+json
Content-Type: application/vnd.company.myapp-v3+json

这是因为html5 enctype属性是一个枚举,因此除通常的application/x-www-formurlencodedmultipart/form-datatext/plain之外的任何内容都是无效的。

…我也不确定HTML4中的所有浏览器都支持它(它有一个更宽松的encytpe属性,但对于mime类型是否被转发则是一个浏览器实现问题)

正因为如此,我现在觉得最合适的版本方法是通过URI,但我接受这不是"正确"的方法。


将您的版本放在URI中。一个API的一个版本并不总是支持来自另一个版本的类型,所以仅仅将资源从一个版本迁移到另一个版本的说法显然是错误的。这与从XML到JSON的转换格式不同。类型可能不存在,或者它们可能在语义上发生了变化。

版本是资源地址的一部分。您正在从一个API路由到另一个API。在标题中隐藏地址是不安全的。


在RESTAPI中,有几个地方可以进行版本控制:

  • 如前所述,在URI中。如果能很好地使用重定向和类似的方法,这会很容易处理,甚至美观。

  • 在accepts:header中,所以版本在filetype中。比如"MP3"和"MP4"。这也会有效,尽管我觉得它比…

  • 在资源本身。许多文件格式中都嵌入了它们的版本号,通常是在头文件中;这允许较新的软件通过理解文件类型的所有现有版本来"仅仅工作",而较旧的软件可以在指定了不受支持的(较新的)版本时进行Punt。在RESTAPI的上下文中,这意味着您的URI永远都不需要更改,只需要您对所传递数据的特定版本的响应。

  • 我可以看到使用这三种方法的原因:

  • 如果您喜欢"清理"新的API,或者在需要这种方法的地方对主要版本进行更改。
  • 如果你想让客户在做一个Put/Post之前知道它是否会工作。
  • 如果客户必须通过投出/发帖来确定是否可以工作,那就好了。

  • 对RESTAPI进行版本控制类似于对任何其他API进行版本控制。小的更改可以就地完成,大的更改可能需要一个全新的API。对您来说,最简单的方法是每次从头开始,也就是将版本放到URL中最有意义。如果你想让客户的生活更容易,你可以试着保持向后的兼容性,这可以通过拒绝(永久重定向)、多个版本中的资源等来实现。这更为复杂,需要付出更多的努力。但这也是REST在"酷的URI不变"中鼓励的。

    最后,它就像其他API设计一样。权衡工作与客户的便利性。考虑为您的API采用语义版本控制,这使您的客户清楚地知道新版本向后兼容的程度。