关于restful:RESTful接口设计译

3次阅读

共计 15663 个字符,预计需要花费 40 分钟才能阅读完成。

原文链接:Web API design best practices – Azure Architecture Center | Microsoft Docs

当初网络上曾经有了很多服务商的公开 API,能够让各类客户端调用,那么怎么才是一个设计低劣的 web API 呢?一般来讲应该具备以下规范:

  1. 平台无关性:应用 API 的能够是任何客户端,它们不必关怀 API 是怎么实现的。这就要求了交互时应用到的协定要标准化,并且要存在一种机制,能确保客户端和服务提供方在数据格式上达成统一。
  2. 服务演变: web API 能够自行更新迭代本人的性能,应用它的客户端不必做出任何批改就能持续应用这些 API。服务端提供的所有性能要具备可发现性,使得客户端能充沛应用到它们。

上面来说说设计 web API 时要思考的一些关键问题。

什么是 REST?

在 2000 年,Roy Fielding 提出应用表述性状态转移(Representational State Transfer,简称 REST)来设计网络服务的构建办法。REST 是一种基于超媒体来构建分布式系统的架构格调,它不应关注底层服务如何,也不必跟 HTTP 绑定,不过大部分 REST API 的实现还是基于了 HTTP 协定。让咱们先关注下如何应用 HTTP 来设计 REST API 接口。

在 HTTP 上应用 REST 的益处是它是一个有公开规范的协定,不须要这些 API 的提供方或应用方依赖任何特定实现计划,服务方和应用方能够用任意语言、工具包来提供 REST 服务实现,或创立 HTTP 申请以及解析 HTTP 响应报文。

基于 HTTP 设计 RESTful API 的次要准则有:

  • REST API 的外围是资源,能够是客户能拜访到的任意物体、数据或服务
  • 每种资源都要有一个举世无双的 URI 来作为惟一标识符定位到该资源。比方一种客户订单能够这样形容:

    https://adventure-works.com/orders/1
  • 客户通过替换资源表述来与服务交互。许多 web API 应用 JSON 作为数据转换格局。例如,一个针对下面 URI 的 GET 申请可能失去如下内容:

    {"orderId":1,"orderValue":99.90,"productId":1,"quantity":1}
  • REST API 应用一套对立的接口,来帮忙解耦调用端和服务实现。在 HTTP 上构建 REST API 时,对立接口应用规范的 HTTP 动词来执行资源的操作,常常应用到的操作有 GET,POST,PUT,PATCH 和 DELETE。
  • REST API 是无状态的。因为收回的 HTTP 申请是独立且无序的,因而没方法在申请间放弃这种长期会话状态。能存储信息的中央只能是 API 资源本身,而每个申请该当是原子操作。这样的束缚要求客户和特定服务器间不能保留任何关联,从而使得服务具备了高度的可伸缩性。任意的服务器能够解决任何的客户申请。然而,其余因素可能会限度这种可伸缩性,比方很多服务都要写数据到后端存储中,而这种繁多存储就很难扩大。对于这种数据存储该如何扩大的策略办法,能够参考 Horizontal, vertical, and functional data partitioning.
  • REST API 的重要驱动外围是其表述内容中蕴含的超媒体链接。举个例子,上面展现了一个蕴含订单信息的 JSON 格局内容,它蕴含了一些链接,用来取得或更新与该订单关联的客户数据。

    {
      "orderID":3,
      "productID":2,
      "quantity":4,
      "orderValue":16.60,
      "links": [{"rel":"product","href":"https://adventure-works.com/customers/3", "action":"GET"},
          {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"PUT"}
      ]
    }

    在 2008 年,Leonard Richardson 提出了一个 web API 成熟度模型:

  • Level 0:仅有一个 URI,用来解决所有 POST 申请 .
  • Level 1:不同的资源提供独自的 URI 申请门路.
  • Level 2:应用 HTTP 的 method 来定义在资源上的不同操作.
  • Level 3:应用了超媒体(HATEOS,上面会提到)

    围绕资源设计 API

    web API 须要裸露一些业务实体,拿电商零碎来举例,次要的实体基本上就是客户和订单了。能够发送一个蕴含了订单信息的 HTTP POST 申请来创立一个订单,而响应报文该当告知这个订单是否创立胜利。请记得,提供进去的这些资源操作 URI 应尽量应用名词(资源名)来形容,而非动词(操作动作)。

    https://adventure-works.com/orders // Good
    
    https://adventure-works.com/create-order // Avoid

    一个所谓的资源不肯定要是一个实在的物理实体,比方对于订单来说,它可能在外部实现上用到了数张关系型数据库中的表,然而对于客户而言,它就是一个独自的实体。不要创立一些仅仅是把数据库对象做了个简略镜像展现的 API。REST 的指标是形容实体和可在其上执行的操作,而客户不应接触到外部实现。

实体通常会有其汇合状态(一组订单、一些顾客)。相比与其中的繁多实体,一个汇合也是一种独自的资源,也应提供其独有的拜访 URI。例如上面这个 URI 就示意了一组订单:

https://adventure-works.com/orders

应用 HTTP GET 调用该 URI,就会失去一组数据,而其中每一个元素也应有其独自的拜访 URI,通过 HTTP GET 办法拜访它们,就能失去每一个元素的详细信息。

采纳一个对立的命名标准来定义这些 URI。通常提供的内容是汇合时应该应用名字的复数模式。一个很好的做法是在汇合和元素间提供层级构造,比方应用 /customers 来代表客户汇合,而 /customers/5 则指向 ID 是 5 的这个独自客户。这样做会让你的 web API 很直观。并且很多 web API 框架提供了参数 URI 门路的反对,所以你能够应用 /customers/{id} 来做资源路由。

另外须要思考的是不同资源间的关系,和你应该如何暴露出这种分割。例如咱们晓得 /customers/5/orders 应该代表了顾客 5 的所有订单,然而如果门路是从订单开始,列出其关联的所有客户的 URI 就可能是/orders/99/customer。然而,这种模式的过分扩大实现起来会很繁冗轻便。一个更好的计划是,在 HTTP 的响应报文体中提供关联资源的导航链接。咱们会在上面章节开展具体探讨。

在一些简单零碎中,像 /customers/1/orders/99/products 这样的提供给客户端多级关系门路拜访的 URI,看起来仿佛很不便,然而这样的简单级别会让保护变得艰难,并且难以在未来调整资源间的层级关系。因而,好的做法是让资源 URI 尽量简单明了,一旦应用程序有了对资源的援用,就应该能够应用这个援用来查找与该资源相干的项。比方后面查找客户 1 所有订单的查问 URI 能够替换为 /customers/1/orders,而后通过/orders/99/products 来失去订单中的所有商品。

留神:不要应用比 /collection/item/collection 更简单的查问 URI。

另外要思考每次申请对服务器的负载影响。申请越多,负载越高。因而,不要提供太多过于细小琐碎的资源接口。这样的接口可能须要客户端执行屡次申请能力失去想到的数据。能够思考把一些相干数据组合到一个资源中,以使得一次查问申请便能满足需要。不过,你还是得做好衡量,来防止拿到过多的无用数据。另外,检索的数据过大,还会让申请变得迟缓,并产生额定的带宽老本。跟多对于性能错误模式的介绍,可参看 Chatty I/O 和 Extraneous Fetching.

要防止让 web API 和底层数据库产生依赖关系,例如,当你应用了关系数据库存储数据,web API 并不需要把所有的表都裸露为资源汇合,这样设计很不好,适合的做法是,把 web API 当成是数据库的形象,或者能够思考应用一个映射层来建设数据库和 web API 的映射关系。这样客户端就能够与底层数据库隔离开来,从而在底层数据表变动时不受到影响。

最初,可能不太容易做到将所有 web API 实现的操作找到适合的资源形容来建设映射关系,一些场景里,收回的 HTTP 申请只是用来执行了某个函数,而后将后果作为 HTTP 响应报文返回,比方用作简略加减计算的 web API,可能会应用伪资源作为 URI,并将查问字符串当作计算的参数。例如,一个 URI 为 /add?operand1=99&operand2 的 GET 申请,会失去一个报文内容为 100 的返回后果。不过最好还是有节制的应用这种模式的 URIs。

通过 HTTP 办法定义 API 操作

HTTP 协定定义了一些有非凡语义的申请办法,在 RESTful 接口中最罕用到的有:

  • GET 获取指定 URI 表述的资源数据,响应报文里蕴含申请资源的具体内容。
  • POST 应用指定 URI 创立资源对象,并返回对象的具体内容。留神 POST 有时也用来触发实际上并不会创立资源的操作。
  • PUT 依据申请音讯的不同指定,创立或更新指定 URI 的资源对象
  • PATCH 能够在申请体中指定一系列变更内容,来执行对应资源的局部更新动作
  • DELETE 应用指定 URI 删除资源

资源是汇合还是独自个体,会使得申请的成果有所不同。上面总结了一些电商零碎中常见的 RESTful 实现常规。有些申请没有解决 – 这取决于场景。

Resource POST GET PUT DELETE
/customers 创立一个新客户 检索所有客户 批量更新客户 删除所有客户
/customers/1 检索客户 1 的详细信息 更新客户 1 的详细信息(若存在) 删除客户 1
customers/1/orders 为客户 1 创立一个新订单 检索客户 1 的所有订单 批量更新客户 1 的所有订单 删除客户 1 的所有订单

强调下 POST、PUT、PATCH 办法的差别:

  • POST 申请会创立一个资源,服务端会给新创建的资源分配一个 URI,并将其返回给客户端。在 REST 模式下,常常会对汇合应用 POST 操作,新资源被创立后便被退出到汇合中。POST 申请也用在提交数据给现存资源来实现一些操作,过程中并不会有新资源被创立。
  • PUT 申请用来创立新资源或更新曾经存在的资源。客户端指定资源的 URI,并在申请体中蕴含资源的残缺示意。如果指定的资源曾经存在,它便会被申请内容替换掉,否则便会创立一个新资源(如果服务端反对该操作)。PUT 申请更多的用在独自资源上,而不是汇合资源,比方一个特定的客户。服务端通常会反对 PUT 申请的更新操作,但不肯定会反对创立资源,这取决于客户端是否能够在资源不存在时候派新 URI 给它。如果不反对,请应用 POST 来创立新资源,而后用 PUT 或 PATCH 来更新它。
  • PATCH 申请用来执行已存在资源的局部更新。客户端指定资源的 URI,并在申请体中携带须要执行变更的内容汇合。这种做法会比 PUT 更无效,鉴于客户端只须要发送变更的局部,而不是整个资源数据。从技术上来说 PATCH 也能够创立资源,通过把所有资源变更数据都指定为 null 来实现,不过最终要看服务端是否反对这种做法。

PUT 申请必须是幂等的。如果客户端发送了屡次同样的申请,失去的后果应该是一样的(同样的资源被变更为了同样的内容)。POST 和 PATCH 则没有这样的要求。

恪守 HTTP 语义

本节介绍 HTTP 标准下须要思考的一些常见注意事项,没有涵盖所有可能细节和场景,如有疑难请参阅 HTTP 标准。

Media types

下面提到,客户端和服务端会替换资源数据。比方在 POST 申请中,申请体需蕴含要创立资源的表述,而 GET 申请中,响应报文体中也会蕴含检索资源的表述内容。

在 HTTP 协定里,格局通过 media types 来指定,也被称为 MIME types。对于非二进制数据,大部分 web API 提供了 JSON(media type = application/json)或者 XML(media type = application/xml)格局的反对。

在申请或响应中通过 Content-Type 音讯头来指定数据格式。上面有一个蕴含了 JSON 数据的 POST 申请示例:

POST https://adventure-works.com/orders HTTP/1.1
Content-Type: application/json; charset=utf-8
Content-Length: 57

{"Id":1,"Name":"Gizmo","Category":"Widgets","Price":1.99}

如果服务端不反对申请的 media type,那么它应该返回 415 这个 HTTP 状态码(Unsupported Media Type)。

客户端申请能够携带一个 Accept 的音讯头来向服务端表明它能够承受报文的 media type 列表,例如:

GET https://adventure-works.com/orders/2 HTTP/1.1
Accept: application/json

如果服务端不能满足列出的 media type 申请,它应该返回 HTTP 状态码 406(Not Acceptable)。

GET 办法

GET 申请胜利后通常应返回状态码 200(OK)。如果申请的资源不存在,则应返回 404(Not Found)。

POST 办法

如果 POST 申请创立了新资源,它应返回 HTTP 状态码 201(Created)。新创建资源的 URI 应在响应报文头 Location 中携带,而报文体则应蕴含该资源的表述。

如果办法做了一些解决,然而并没有创立新资源,它同样能够返回 200 状态码,并把处理结果放在响应报文中,而当并没有任何处理结果须要返回时,能够应用 204 状态码 (No Content) 来代替,并返回空响应报文。

如果客户端在申请中携带了有效数据,服务端应该返回状态码 400(Bad Request),响应报文中则能够放入对于谬误的具体阐明信息,或者提供一个链接以用来查看更多信息。

PUT 办法

如果 PUT 办法创立了新的资源,它会返回 201 状态码 (Created),跟 POST 一样。如果它更新了一个存在的资源,能够返回 200(OK) 或者 204(No Content)。某些状况下,可能无奈更新指定的资源,这时能够思考返回 HTTP 状态码 409(Conflict)。

能够思考提供一个批量更新 PUT 办法,在办法体里放上要更新的所有资源内容,并应用 URI 指明要更新的资源汇合。这样能升高网络开销和进步性能。

PATCH 办法

应用 PATCH 申请时,客户端应用一种 补丁文件 的格局,向曾经存在的数据资源发送更新申请。服务端会应用这个补丁文件解决更新。补丁文件并不需要形容资源的所有内容,只须要通知更新哪些局部即可。PATCH 办法的标准 (RFC 5789) 并没有定义这个补丁文件要有什么样的特定格局,格局须要依据申请的 media type 而定。

JSON 应该是 web API 届最通用的数据格式了。而 patch 办法用到的基于 JSON 的补丁文件格式有两个,叫做 JSON patch 和 JSON merge patch。

JSON merge patch 绝对简略一点。它的构造就像是原始资源对象的 JSON 模式内容,只是它蕴含的仅仅是须要更新或者增加的数据字段汇合,在补丁文件中通过将字段指定为 null 甚至还能够删除字段(不过不适用于原始资源能够显式的蕴含 ’null’ 值的状况)

举个例子,如果原始资源对象的 JSON 模式内容为:

{
    "name":"gizmo",
    "category":"widgets",
    "color":"blue",
    "price":10
}

可能对应的更新文件的内容会是这样:

{
    "price":12,
    "color":null,
    "size":"small"
}

这个文件通知服务端,要更新 price,删掉 color,并增加 size 字段,同时 namecategory 字段没有任何改变。无关 JSON merge patch 的更多具体内容能够参考 RFC 7396。它对应的 media type 为 application/merge-patch+json

Merge patch 并不适用于本来资源中蕴含显式 ’null’ 值的状况,因为补丁文件中的 null 有非凡含意(代表删除)。并且补丁文件不能指定更新被执行的程序,不过这个有没有具体影响要看数据对象的解决逻辑。在 RFC 6902 中定义的 JSON patch,绝对就灵便一些。它能够指明具体要执行的操作序列,操作的类型能够是减少、删除、替换、拷贝和测试(用来验证特定值)。它的 media type 是 application/json-patch+json

下表列出了一些解决 PATCH 申请时可能会遇到的一些典型谬误条件,和对应适当的 HTTP 状态码。

Error condition HTTP status code
The patch document format isn’t supported. 415 (Unsupported Media Type)
Malformed patch document. 400 (Bad Request)
The patch document is valid, but the changes can’t be applied to the resource in its current state. 409 (Conflict)

DELETE 办法

如果一个删除申请被胜利执行,web 服务端应该返回一个 204 状态码(No Content),这代表着执行曾经胜利,响应报文不须要有任何内容。如果申请的资源不存在,应该失去一个 HTTP 404(Not Found)。

异步操作

有时一个 POST、PUT、PATCH 或者 DELETE 操作可能会须要执行一段时间,如果在胜利相应之前始终期待,过长的提早可能不太好承受。这时能够思考将操作改成异步来执行。返回一个 202(Accepted)状态码来表明申请曾经承受并在解决中,然而还没有实现。

你应该裸露一个端点,来提供异步申请执行状态的查问,这样客户端就能轮询它来监测状态。能够在 202 报文头中蕴含这个状态端点的地址,如:

HTTP/1.1 202 Accepted
Location: /api/status/12345

如果客户端向该端点发动了一个 GET 申请,响应报文中应该蕴含操作的执行状态。另外,也能够提供其余一些信息,比方预计要耗费的工夫和一个能够勾销操作的链接。

HTTP/1.1 200 OK
Content-Type: application/json

{
    "status":"In progress",
    "link": {"rel":"cancel", "method":"delete", "href":"/api/status/12345"}
}

如果异步操作创立了新的资源,那么这个状态端点应该在操作执行实现后返回一个 303(See Other),并在响应报文头信息中提供新资源的拜访地址。

HTTP/1.1 303 See Other
Location: /api/orders/12345

更多相干信息,可参考 Asynchronous Request-Reply pattern.

数据过滤和分页

提供一个能够查问资源汇合的 URI,可能会呈现客户端其实只须要一小部分数据,但却发动了一个很大数据量的申请。比方,客户程序须要查问所有老本大于某个值的所有订单,那么可能它会先申请 /orders 这个 URI 拿到所有订单数据,而后在客户端把这些数据作一下过滤。很显然这样解决十分的不效率,而且节约了贷款和服务器算力。

绝对的,这个 API 也能够提供申请 URI 时携带查问字符串的反对,比方 /orders?minCost=n。而后服务端解析解决这个 minCost 查问项,并返回过滤后的后果。

一个对资源汇合的 GET 申请总是有可能会返回一大堆数据,所以最好在你的 web API 中提供对单次申请返回后果数量的限度。能够思考提供一个最大数量申请查问参数,来指明能失去的最大后果数量,同时须要提供一个偏移量参数,例如:

/orders?limit=25&offset=50

同时为了防止可能受到的拒绝服务攻打,要对这个最大数量参数设置个下限。另外,为了帮助客户应用程序实现分页配置,服务端对 GET 申请返回分页数据时,要在响应报文中蕴含一些元数据,用来表明资源可用的总页数等。

相似的,还能够提供一个 sort 参数用来对申请数据排序,能够应用后果字段中的任意一个,比方 /orders?sort=ProductID。不过这种做法有一个害处,因为有些缓存实现用查问字符串作为缓存的 key,sort 参数的调整可能会让之前的缓存生效。

在查问数据的字段数量特地多时,还能够对返回字段作一下限度。提供一个反对逗号分隔的查问字符串参数来示意须要的字段列表,比方 /orders?fields=ProductID,Quantity

对每一个反对的查问字符串参数,提供一个有意义的默认值。例如,如果反对分页,最好将数据条数默认为 10,偏移量(页码)默认为 0。如果反对排序,将 sort 列默认为资源的主键,如果反对投影(可选查问列),将参数默认为所有列。

大型二进制文件的局部响应

有些资源可能蕴含二进制字段,比方图片和文件。这些内容在不稳固网络环境下传输可能会呈现问题,比方连贯中断,或者解决工夫过长,为了克服这些问题,能够思考分块获取。首先,API 须要对 GET 申请中的 Accept-Ranges 这个用来申请大文件资源的音讯头提供反对,这个音讯头表明 GET 操作反对局部申请,客户端能够发动一个指定了字节数范畴的 GET 申请,来失去一部分资源数据。

同时,最好也实现对这些资源的 HTTP HEAD 申请解决。HEAD 申请相似于 GET,只不过它只返回形容资源的 HTTP 音讯头,而音讯体是空的。客户程序能够收回 HEAD 申请来判断是否须要应用 GET 局部申请形式获取资源。例如:

HEAD https://adventure-works.com/products/10?fields=productImage HTTP/1.1

对应的响应报文为:

HTTP/1.1 200 OK

Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 4580

Content-Length 报文头给出了资源的总大小,而 Accept-Ranges 报文头则表明对应的 GET 操作反对局部后果的申请。有了这些信息,客户程序便能够把图片分成小块获取。第一个申请应用了 Range 头获取了 2500 个字节:

GET https://adventure-works.com/products/10?fields=productImage HTTP/1.1
Range: bytes=0-2499

响应报文应用 HTTP 状态码 206 来表明这是残缺内容的一部分。Content-Length 报文头阐明了音讯体内的理论字节数(而非资源的残缺大小),Content-Range 报文头则表明这是整个资源的哪一部分(残缺 4580 中的 0 -2499 这部分)

HTTP/1.1 206 Partial Content

Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 2500
Content-Range: bytes 0-2499/4580

[...]

客户程序能够发动后续申请来取得资源的残余局部。

应用资源导航(HATEOAS)

REST 有一个推崇的概念,该当在不须要先理解 URI 计划的状况下,做到对整个资源集的导航。每一个 HTTP GET 申请的响应报文,都应该蕴含一系列超链接信息,用来获取跟申请对象有间接关系的资源,并提供这些资源上的可用操作阐明。这个准则被称为 HATEOAS,即 Hypertext as the Engine of Application State。这个零碎实际上是一种无限状态机,申请的响应报文蕴含了从一种状态转移到另一种状态的必要信息,跟状态转换无关的内容则不应蕴含在内。

以后并没有针对 HATEOAS 准则的通用建模规范,本节示例只展现了一种特定用处下的计划。

比方,解决订单和客户关系时,能够在订单的体现上蕴含一些链接,用来标识跟这个订单相干客户的一些可用操作。比方:

{
  "orderID":3,
  "productID":2,
  "quantity":4,
  "orderValue":16.60,
  "links":[
    {
      "rel":"customer",
      "href":"https://adventure-works.com/customers/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"customer",
      "href":"https://adventure-works.com/customers/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"customer",
      "href":"https://adventure-works.com/customers/3",
      "action":"DELETE",
      "types":[]},
    {
      "rel":"self",
      "href":"https://adventure-works.com/orders/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"self",
      "href":"https://adventure-works.com/orders/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"self",
      "href":"https://adventure-works.com/orders/3",
      "action":"DELETE",
      "types":[]}]
}

在这个例子里,links 这个数组节点蕴含了一个链接数据汇合,每个链接数据都代表了一个相干实体的可用操作。每个链接数据又蕴含了关系(“customer”)、URI(https://adventure-works.com/c…)、HTTP 办法,和可用的 MIME 类型。这些信息足够一个客户程序用来执行操作了。

链接数组里还蕴含了一些自援用信息,跟本来获取到的申请资源无关,它们的关系被标记为 ‘self’。

依据不同的资源状态,links 数组节点中的内容可能会有所不同。这也正是超文本作为 “engine of application state.” 的含意。

RESTful web API 的版本控制

通常 web API 不会变化无穷,随着业务需要变动,会有更多资源汇合呈现,资源之间的关系会变动,资源的数据结构也会有所扭转。更新 web API 来解决变动的需要或者没什么难度,然而必须得思考这些改变对于那些正在应用这些 API 的客户端程序的影响。设计和保护这些 web API 的程序员,对这些接口有齐全的控制权,然而对那些客户端程序可并不一定是这样,有可能开发它们的是一些远处的第三方。所以要害的是能让现存的客户端程序持续失常运行,同时新的客户端程序能够充沛应用这些新性能和资源。

版本控制能够使 web API 表明它公开的性能和资源,同时能让客户端程序向性能和资源的某个特定版本发动申请。上面几节形容了不同的版本控制办法,各有其优劣。

无版本控制

这是最简略的办法,一些外部调用 API 能够承受这么做。重大的改变个别体现为新的资源或新链接,对曾经存在的资源增加内容个别也不会有什么问题,因为申请端程序遇到冀望之外的数据内容,个别会主动疏忽掉。

比方,向 URI https://adventure-works.com/customers/3 发动申请,会返回一个蕴含了 id、name 和 address 字段的单个客户明细数据,这些数据合乎申请端程序的预期:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

简略起见,本节示例响应报文并没有蕴含 HATEOAS 链接.

如果向客户资源的构造中增加了 DateCreated 字段,响应内容会变为:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":"1 Microsoft Way Redmond WA 98053"}

曾经存在的客户程序会持续失常运行,只有它们能够疏忽掉不意识的字段,而新的客户程序能够设计为可能解决这个新增加的字段。不过,如果资源的构造产生了更彻底的改变(比方删除或者重命名了某些字段),或者资源间的关系产生了变动,那就有可能使得这些正在运行的客户程序无奈再正确运行。这种状况下,你就得思考上面的这些办法了。

URI 版本控制

每当对 web API 或者资源构造进行改变时,能够对每个资源的 URI 增加一个版本号,先前的 URI 应持续像之前那样运作,返回本来的资源数据。

对后面的例子作下扩大,如果将 address 字段重构为蕴含不同组成部分的子字段(比方街道、城市、省份和邮政编码),这个版本的资源能够公布为一个蕴含了版本号的 URI,例如 https://adventure-works.com/v2/customers/3:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}

这种版本控制机制非常简单,然而它依赖于服务器能将申请路由到正确的服务端点。而且,随着 web API 的逐渐迭代和服务器提供反对的版本数量增多,它会变得很轻便。另外,从纯正主义角度去看,返回了同样数据(客户 3)的 URI,它们的版本不应该是不一样的。这个计划还会让 HATEOAS 的实现更加简单,因为所有链接都不得不将版本号蕴含在它们的 URI 中。

查问字符串版本控制

相比于提供多个不通的 URI,更好的做法可能是在 HTTP 申请上附带一个指定了资源版本的查问字符串参数,比方 https://adventure-works.com/customers/3?version=2。给这个版本参数一个有意义的默认值,比方 1,这样旧的客户程序能够疏忽掉它而不受到影响。

这种做法的益处是,在语义上,同样的资源申请用了雷同的 URI,不过这须要相应的代码正确的解析申请中的查问字符串,并给与正确的 HTTP 响应。另外此办法同样会遇到跟 URI 版本控制一样的问题,即实现 HATEOAS 会比拟麻烦。

一些旧版浏览器和网络代理不会缓存蕴含查问字符串的申请响应内容,在其上运行的网络程序调用 web API 时的性能可能会受到影响。

音讯头版本控制

除了将版本号放到查问字符串参数中,还能够实现一个自定义音讯头来指明须要的资源版本。这个做法须要客户端程序在所有申请上携带正确的音讯头,只管服务代码能够在申请疏忽了这个音讯头时给一个默认值(比方版本 1)。上面这个例子应用了名为 Custom-Header 的音讯头,外面的值指明了 web API 的版本。

Version 1:


GET https://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

Version 2:

GET https://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=2
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}

和后面两种版本控制计划一样,实现 HATEOAS 也须要在所有链接中带上正确的音讯头。

媒体类型版本控制

在后面有介绍过,当客户端程序收回一个 HTTP GET 申请到 web 服务端时,它该当应用 Accept 音讯头明确它能够解决的内容格局。Accept 音讯头常常用来指明客户端程序心愿失去的响应报文格式应该是 XML、JSON 或者其余一些通用格局。不过,也能够扩大一些自定义媒体类型,使得客户程序能够在其中加一些信息来指明须要的资源版本。

上面这个例子在 Accept 音讯头中指定了 application/vnd.adventure-works.v1+json,其中的 vnd.adventure-works.v1 就向 web 服务端表明了它须要的是 1 这个资源版本,并心愿失去 json 格局的响应内容:

GET https://adventure-works.com/customers/3 HTTP/1.1
Accept: application/vnd.adventure-works.v1+json

解决申请的代码须要解决这个音讯头,并尽可能的满足它(客户端程序可能会在 Accept 中指定多个格局,服务端只须要抉择其中最合适的一个作为响应格局即可)。Web 服务端在响应体中应用 Content-Type 报文头来对数据格式进行确认:

HTTP/1.1 200 OK
Content-Type: application/vnd.adventure-works.v1+json; charset=utf-8

{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

如果 Accept 音讯头中指定了无奈解决的媒体类型,服务端能够返回 HTTP 状态码 406(Not Acceptable),或者应用一个默认类型来解决。

这种做法被认为是最纯正的版本控制计划了(译者猜应该是说这种做法能够在连贯上只是明确指向资源即可,不须要做其余调整,URI 版本计划,和查问字符串版本计划,都会影响这个 URI 的洁净度),并且它原生反对 HATEOAS,因为能够在链接节点中指明 MIME 类型。

在抉择一种版本控制计划时,须要同时思考到对性能的潜在影响,特地是在 web 服务端的缓存解决。URI 和查问字符串版本控制计划是能够缓存的,因为携带了版本信息的残缺 URI 和查问字符串会作为缓存的惟一标识,同样的版本申请每次都指向同样的数据。

The Header versioning and Media Type versioning mechanisms typically require additional logic to examine the values in the custom header or the Accept header. In a large-scale environment, many clients using different versions of a web API can result in a significant amount of duplicated data in a server-side cache. This issue can become acute if a client application communicates with a web server through a proxy that implements caching, and that only forwards a request to the web server if it does not currently hold a copy of the requested data in its cache.(这段译者未能齐全了解,猜其意思,可能跟代理的缓存策略无关,因为 Header 和 MediaType 的版本控制计划,在申请不同版本时的 URI 和 query string 并没有变动,代理可能会应用不正确的缓存数据间接返回,而不是向 web 服务端发动新的申请来获取数据)

Open API 打算

Open API 打算是由一个行业联盟创立的,目标是标准化供应商之间的 REST API 形容。作为这一打算的一部分,Swagger 2.0 标准被重新命名为 OpenAPI 标准(OAS),并纳入了 Open API 打算。

如果你想要把本人的 web API 革新为 OpenAPI,能够参考上面几点:

  • OpenAPI 标准附带了一套对于 REST API 应该如何设计的执著的指导方针。这对互操作性有益处,但在设计 API 以符合规范时须要更加小心。
  • OpenAPI 提倡契约优先的办法,而不是实现优先的办法。契约优先意味着首先设计 API 契约(接口),而后编写实现该契约的代码。
  • Swagger 这样的工具能够从 API 契约间接生成客户端代码或文档。可参考 ASP.NET Web API help pages using Swagger。

More information

  • Microsoft REST API guidelines. Detailed recommendations for designing public REST APIs.
  • Web API checklist. A useful list of items to consider when designing and implementing a web API.
  • Open API Initiative. Documentation and implementation details on Open API.
正文完
 0