原文鏈接:Web API design best practices - Azure Architecture Center | Microsoft Docs
現(xiàn)在網(wǎng)絡(luò)上已經(jīng)有了很多服務(wù)商的公開(kāi)API,可以讓各類客戶端調(diào)用,那么怎樣才是一個(gè)設(shè)計(jì)優(yōu)良的web API呢?一般來(lái)講應(yīng)該具備以下標(biāo)準(zhǔn):
平臺(tái)無(wú)關(guān)性:使用API的可以是任何客戶端,它們不用關(guān)心API是怎么實(shí)現(xiàn)的。這就要求了交互時(shí)使用到的協(xié)議要標(biāo)準(zhǔn)化,并且要存在一種機(jī)制,能確保客戶端和服務(wù)提供方在數(shù)據(jù)格式上達(dá)成一致。
服務(wù)演化: web API可以自行更新迭代自己的功能,使用它的客戶端不用做出任何修改就能繼續(xù)使用這些API。服務(wù)端提供的所有功能要具備可發(fā)現(xiàn)性,使得客戶端能充分使用到它們。
下面來(lái)說(shuō)說(shuō)設(shè)計(jì)web API時(shí)要考慮的一些關(guān)鍵問(wèn)題。
什么是REST?
在2000年,Roy Fielding提出使用表述性狀態(tài)轉(zhuǎn)移(Representational State Transfer ,簡(jiǎn)稱REST)來(lái)設(shè)計(jì)網(wǎng)絡(luò)服務(wù)的構(gòu)建方法。REST是一種基于超媒體來(lái)構(gòu)建分布式系統(tǒng)的架構(gòu)風(fēng)格,它不應(yīng)關(guān)注底層服務(wù)如何,也不用跟HTTP綁定,不過(guò)大部分REST API的實(shí)現(xiàn)還是基于了HTTP協(xié)議。讓我們先關(guān)注下如何使用HTTP來(lái)設(shè)計(jì)REST API接口。
在HTTP上使用REST的好處是它是一個(gè)有公開(kāi)標(biāo)準(zhǔn)的協(xié)議,不需要這些API的提供方或使用方依賴任何特定實(shí)現(xiàn)方案,服務(wù)方和使用方可以用任意語(yǔ)言、工具包來(lái)提供REST服務(wù)實(shí)現(xiàn),或創(chuàng)建HTTP請(qǐng)求以及解析HTTP響應(yīng)報(bào)文。
基于HTTP設(shè)計(jì)RESTful API的主要原則有:
- REST API的核心是資源,可以是客戶能訪問(wèn)到的任意物體、數(shù)據(jù)或服務(wù)
- 每種資源都要有一個(gè)獨(dú)一無(wú)二的URI來(lái)作為唯一標(biāo)識(shí)符定位到該資源。比如一種客戶訂單可以這樣描述:
https://adventure-works.com/orders/1
- 客戶通過(guò)交換資源表述來(lái)與服務(wù)交互。許多web API使用JSON作為數(shù)據(jù)轉(zhuǎn)換格式。例如,一個(gè)針對(duì)上面URI的GET請(qǐng)求可能得到如下內(nèi)容:
{"orderId":1,"orderValue":99.90,"productId":1,"quantity":1}
REST API使用一套統(tǒng)一的接口,來(lái)幫助解耦調(diào)用端和服務(wù)實(shí)現(xiàn)。在HTTP上構(gòu)建REST API時(shí),統(tǒng)一接口使用標(biāo)準(zhǔn)的HTTP動(dòng)詞來(lái)執(zhí)行資源的操作,經(jīng)常使用到的操作有GET,POST,PUT,PATCH和DELETE。
REST API是無(wú)狀態(tài)的。由于發(fā)出的HTTP請(qǐng)求是獨(dú)立且無(wú)序的,因此沒(méi)辦法在請(qǐng)求間保持這種臨時(shí)會(huì)話狀態(tài)。能存儲(chǔ)信息的地方只能是API資源自身,而每個(gè)請(qǐng)求應(yīng)當(dāng)是原子操作。這樣的約束要求客戶和特定服務(wù)器間不能保留任何關(guān)聯(lián),從而使得服務(wù)具備了高度的可伸縮性。任意的服務(wù)器可以處理任何的客戶請(qǐng)求。但是,其他因素可能會(huì)限制這種可伸縮性,比如很多服務(wù)都要寫(xiě)數(shù)據(jù)到后端存儲(chǔ)中,而這種單一存儲(chǔ)就很難擴(kuò)展。關(guān)于這種數(shù)據(jù)存儲(chǔ)該如何擴(kuò)展的策略方法,可以參考Horizontal, vertical, and functional data partitioning.
REST API的重要驅(qū)動(dòng)核心是其表述內(nèi)容中包含的超媒體鏈接。舉個(gè)例子,下面展示了一個(gè)包含訂單信息的JSON格式內(nèi)容,它包含了一些鏈接,用來(lái)獲得或更新與該訂單關(guān)聯(lián)的客戶數(shù)據(jù)。
{
"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提出了一個(gè)web API成熟度模型:
- Level 0:僅有一個(gè)URI,用來(lái)處理所有POST請(qǐng)求 .
- Level 1:不同的資源提供單獨(dú)的URI請(qǐng)求路徑.
- Level 2:使用HTTP的method來(lái)定義在資源上的不同操作.
- Level 3:使用了超媒體(HATEOS,下面會(huì)提到)
圍繞資源設(shè)計(jì)API
web API需要暴露一些業(yè)務(wù)實(shí)體, 拿電商系統(tǒng)來(lái)舉例,主要的實(shí)體基本上就是客戶和訂單了。可以發(fā)送一個(gè)包含了訂單信息的HTTP POST請(qǐng)求來(lái)創(chuàng)建一個(gè)訂單,而響應(yīng)報(bào)文應(yīng)當(dāng)告知這個(gè)訂單是否創(chuàng)建成功。請(qǐng)記得,提供出來(lái)的這些資源操作URI應(yīng)盡量使用名詞(資源名)來(lái)描述,而非動(dòng)詞(操作動(dòng)作)。
https://adventure-works.com/orders // Good
https://adventure-works.com/create-order // Avoid
一個(gè)所謂的資源不一定要是一個(gè)真實(shí)的物理實(shí)體,比如對(duì)于訂單來(lái)說(shuō),它可能在內(nèi)部實(shí)現(xiàn)上用到了數(shù)張關(guān)系型數(shù)據(jù)庫(kù)中的表,但是對(duì)于客戶而言,它就是一個(gè)單獨(dú)的實(shí)體。不要?jiǎng)?chuàng)建一些僅僅是把數(shù)據(jù)庫(kù)對(duì)象做了個(gè)簡(jiǎn)單鏡像展示的API。REST的目標(biāo)是描述實(shí)體和可在其上執(zhí)行的操作,而客戶不應(yīng)接觸到內(nèi)部實(shí)現(xiàn)。
實(shí)體通常會(huì)有其集合形態(tài)(一組訂單、一些顧客)。相比與其中的單一實(shí)體,一個(gè)集合也是一種單獨(dú)的資源,也應(yīng)提供其獨(dú)有的訪問(wèn)URI。例如下面這個(gè)URI就表示了一組訂單:
https://adventure-works.com/orders
使用HTTP GET調(diào)用該URI,就會(huì)得到一組數(shù)據(jù),而其中每一個(gè)元素也應(yīng)有其單獨(dú)的訪問(wèn)URI,通過(guò)HTTP GET方法訪問(wèn)它們,就能得到每一個(gè)元素的詳細(xì)信息。
采用一個(gè)統(tǒng)一的命名規(guī)范來(lái)定義這些URI。通常提供的內(nèi)容是集合時(shí)應(yīng)該使用名字的復(fù)數(shù)形式。一個(gè)很好的做法是在集合和元素間提供層級(jí)結(jié)構(gòu),比如使用/customers
來(lái)代表客戶集合,而/customers/5
則指向ID是5的這個(gè)單獨(dú)客戶。這樣做會(huì)讓你的web API很直觀。并且很多web API框架提供了參數(shù)URI路徑的支持,所以你可以使用/customers/{id}
來(lái)做資源路由。
另外需要考慮的是不同資源間的關(guān)系,和你應(yīng)該如何暴露出這種聯(lián)系。例如我們知道/customers/5/orders
應(yīng)該代表了顧客5的所有訂單,但是如果路徑是從訂單開(kāi)始,列出其關(guān)聯(lián)的所有客戶的URI就可能是/orders/99/customer
。然而,這種模式的過(guò)分?jǐn)U展實(shí)現(xiàn)起來(lái)會(huì)很繁雜笨重。一個(gè)更好的方案是,在HTTP的響應(yīng)報(bào)文體中提供關(guān)聯(lián)資源的導(dǎo)航鏈接。我們會(huì)在下面章節(jié)展開(kāi)詳細(xì)探討。
在一些復(fù)雜系統(tǒng)中,像/customers/1/orders/99/products
這樣的提供給客戶端多級(jí)關(guān)系路徑訪問(wèn)的URI,看起來(lái)似乎很方便,但是這樣的復(fù)雜級(jí)別會(huì)讓維護(hù)變得困難,并且難以在將來(lái)調(diào)整資源間的層級(jí)關(guān)系。因此,好的做法是讓資源URI盡量簡(jiǎn)單明了,一旦應(yīng)用程序有了對(duì)資源的引用,就應(yīng)該可以使用這個(gè)引用來(lái)查找與該資源相關(guān)的項(xiàng)。比如前面查找客戶1所有訂單的查詢URI可以替換為/customers/1/orders
,然后通過(guò)/orders/99/products
來(lái)得到訂單中的所有商品。
注意:不要使用比
/collection/item/collection
更復(fù)雜的查詢URI。
另外要考慮每次請(qǐng)求對(duì)服務(wù)器的負(fù)載影響。請(qǐng)求越多,負(fù)載越高。因此,不要提供太多過(guò)于細(xì)小瑣碎的資源接口。這樣的接口可能需要客戶端執(zhí)行多次請(qǐng)求才能得到想到的數(shù)據(jù)。可以考慮把一些相關(guān)數(shù)據(jù)組合到一個(gè)資源中,以使得一次查詢請(qǐng)求便能滿足需求。不過(guò),你還是得做好權(quán)衡,來(lái)避免拿到過(guò)多的無(wú)用數(shù)據(jù)。另外,檢索的數(shù)據(jù)過(guò)大,還會(huì)讓請(qǐng)求變得緩慢,并產(chǎn)生額外的帶寬成本。跟多關(guān)于性能錯(cuò)誤模式的介紹,可參看Chatty I/O 和 Extraneous Fetching.
要避免讓web API和底層數(shù)據(jù)庫(kù)產(chǎn)生依賴關(guān)系,例如,當(dāng)你使用了關(guān)系數(shù)據(jù)庫(kù)存儲(chǔ)數(shù)據(jù),web API并不需要把所有的表都暴露為資源集合,這樣設(shè)計(jì)很不好,合適的做法是,把web API當(dāng)成是數(shù)據(jù)庫(kù)的抽象,或者可以考慮使用一個(gè)映射層來(lái)建立數(shù)據(jù)庫(kù)和web API的映射關(guān)系。這樣客戶端就可以與底層數(shù)據(jù)庫(kù)隔離開(kāi)來(lái),從而在底層數(shù)據(jù)表變化時(shí)不受到影響。
最后,可能不太容易做到將所有web API實(shí)現(xiàn)的操作找到合適的資源描述來(lái)建立映射關(guān)系,一些場(chǎng)景里,發(fā)出的HTTP請(qǐng)求只是用來(lái)執(zhí)行了某個(gè)函數(shù),然后將結(jié)果作為HTTP響應(yīng)報(bào)文返回,比如用作簡(jiǎn)單加減計(jì)算的web API,可能會(huì)使用偽資源作為URI,并將查詢字符串當(dāng)作計(jì)算的參數(shù)。例如,一個(gè)URI為/add?operand1=99&operand2的GET請(qǐng)求,會(huì)得到一個(gè)報(bào)文內(nèi)容為100的返回結(jié)果。不過(guò)最好還是有節(jié)制的使用這種形式的URIs。
通過(guò)HTTP方法定義API操作
HTTP協(xié)議定義了一些有特殊語(yǔ)義的請(qǐng)求方法,在RESTful接口中最常用到的有:
- GET 獲取指定URI表述的資源數(shù)據(jù),響應(yīng)報(bào)文里包含請(qǐng)求資源的詳細(xì)內(nèi)容。
- POST 使用指定URI創(chuàng)建資源對(duì)象,并返回對(duì)象的詳細(xì)內(nèi)容。注意POST有時(shí)也用來(lái)觸發(fā)實(shí)際上并不會(huì)創(chuàng)建資源的操作。
- PUT 根據(jù)請(qǐng)求消息的不同指定,創(chuàng)建或更新指定URI的資源對(duì)象
- PATCH 可以在請(qǐng)求體中指定一系列變更內(nèi)容,來(lái)執(zhí)行對(duì)應(yīng)資源的部分更新動(dòng)作
- DELETE 使用指定URI刪除資源
資源是集合還是單獨(dú)個(gè)體,會(huì)使得請(qǐng)求的效果有所不同。下面總結(jié)了一些電商系統(tǒng)中常見(jiàn)的RESTful實(shí)現(xiàn)慣例。有些請(qǐng)求沒(méi)有處理 - 這取決于場(chǎng)景。
Resource | POST | GET | PUT | DELETE |
---|---|---|---|---|
/customers | 創(chuàng)建一個(gè)新客戶 | 檢索所有客戶 | 批量更新客戶 | 刪除所有客戶 |
/customers/1 | 無(wú) | 檢索客戶1的詳細(xì)信息 | 更新客戶1的詳細(xì)信息(若存在) | 刪除客戶1 |
customers/1/orders | 為客戶1創(chuàng)建一個(gè)新訂單 | 檢索客戶1的所有訂單 | 批量更新客戶1的所有訂單 | 刪除客戶1的所有訂單 |
強(qiáng)調(diào)下POST、PUT、PATCH方法的差異:
- POST請(qǐng)求會(huì)創(chuàng)建一個(gè)資源,服務(wù)端會(huì)給新創(chuàng)建的資源分配一個(gè)URI,并將其返回給客戶端。在REST模式下,經(jīng)常會(huì)對(duì)集合使用POST操作,新資源被創(chuàng)建后便被加入到集合中。POST請(qǐng)求也用在提交數(shù)據(jù)給現(xiàn)存資源來(lái)完成一些操作,過(guò)程中并不會(huì)有新資源被創(chuàng)建。
- PUT請(qǐng)求用來(lái)創(chuàng)建新資源或更新已經(jīng)存在的資源。客戶端指定資源的URI,并在請(qǐng)求體中包含資源的完整表示。如果指定的資源已經(jīng)存在,它便會(huì)被請(qǐng)求內(nèi)容替換掉,否則便會(huì)創(chuàng)建一個(gè)新資源(如果服務(wù)端支持該操作)。PUT請(qǐng)求更多的用在單獨(dú)資源上,而不是集合資源,比如一個(gè)特定的客戶。服務(wù)端通常會(huì)支持PUT請(qǐng)求的更新操作,但不一定會(huì)支持創(chuàng)建資源,這取決于客戶端是否可以在資源不存在時(shí)分派新URI給它。如果不支持,請(qǐng)使用POST來(lái)創(chuàng)建新資源,然后用PUT或PATCH來(lái)更新它。
- PATCH請(qǐng)求用來(lái)執(zhí)行已存在資源的部分更新。客戶端指定資源的URI,并在請(qǐng)求體中攜帶需要執(zhí)行變更的內(nèi)容集合。這種做法會(huì)比PUT更有效,鑒于客戶端只需要發(fā)送變更的部分,而不是整個(gè)資源數(shù)據(jù)。從技術(shù)上來(lái)說(shuō)PATCH也可以創(chuàng)建資源,通過(guò)把所有資源變更數(shù)據(jù)都指定為null來(lái)實(shí)現(xiàn),不過(guò)最終要看服務(wù)端是否支持這種做法。
PUT請(qǐng)求必須是冪等的。如果客戶端發(fā)送了多次同樣的請(qǐng)求,得到的結(jié)果應(yīng)該是一樣的(同樣的資源被變更為了同樣的內(nèi)容)。POST和PATCH則沒(méi)有這樣的要求。
遵守HTTP語(yǔ)義
本節(jié)介紹HTTP規(guī)范下需要考慮的一些常見(jiàn)注意事項(xiàng),沒(méi)有涵蓋所有可能細(xì)節(jié)和場(chǎng)景,如有疑問(wèn)請(qǐng)參閱HTTP規(guī)范。
Media types
上面提到,客戶端和服務(wù)端會(huì)交換資源數(shù)據(jù)。比如在POST請(qǐng)求中,請(qǐng)求體需包含要?jiǎng)?chuàng)建資源的表述,而GET請(qǐng)求中,響應(yīng)報(bào)文體中也會(huì)包含檢索資源的表述內(nèi)容。
在HTTP協(xié)議里,格式通過(guò)media types來(lái)指定,也被稱為MIME types。對(duì)于非二進(jìn)制數(shù)據(jù),大部分web API提供了JSON(media type = application/json)或者XML(media type = application/xml)格式的支持。
在請(qǐng)求或響應(yīng)中通過(guò)Content-Type消息頭來(lái)指定數(shù)據(jù)格式。下面有一個(gè)包含了JSON數(shù)據(jù)的POST請(qǐng)求示例:
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}
如果服務(wù)端不支持請(qǐng)求的media type,那么它應(yīng)該返回415這個(gè)HTTP狀態(tài)碼(Unsupported Media Type)。
客戶端請(qǐng)求可以攜帶一個(gè)Accept的消息頭來(lái)向服務(wù)端表明它可以接受報(bào)文的media type列表,例如:
GET https://adventure-works.com/orders/2 HTTP/1.1
Accept: application/json
如果服務(wù)端不能滿足列出的media type請(qǐng)求,它應(yīng)該返回HTTP狀態(tài)碼406(Not Acceptable)。
GET方法
GET請(qǐng)求成功后通常應(yīng)返回狀態(tài)碼200(OK)。如果請(qǐng)求的資源不存在,則應(yīng)返回404(Not Found)。
POST方法
如果POST請(qǐng)求創(chuàng)建了新資源,它應(yīng)返回HTTP狀態(tài)碼201(Created)。新創(chuàng)建資源的URI應(yīng)在響應(yīng)報(bào)文頭Location中攜帶,而報(bào)文體則應(yīng)包含該資源的表述。
如果方法做了一些處理,但是并沒(méi)有創(chuàng)建新資源,它同樣可以返回200狀態(tài)碼,并把處理結(jié)果放在響應(yīng)報(bào)文中,而當(dāng)并沒(méi)有任何處理結(jié)果需要返回時(shí),可以使用204狀態(tài)碼(No Content)來(lái)代替,并返回空響應(yīng)報(bào)文。
如果客戶端在請(qǐng)求中攜帶了無(wú)效數(shù)據(jù),服務(wù)端應(yīng)該返回狀態(tài)碼400(Bad Request),響應(yīng)報(bào)文中則可以放入關(guān)于錯(cuò)誤的詳細(xì)說(shuō)明信息,或者提供一個(gè)鏈接以用來(lái)查看更多信息。
PUT方法
如果PUT方法創(chuàng)建了新的資源,它會(huì)返回201狀態(tài)碼(Created),跟POST一樣。如果它更新了一個(gè)存在的資源,可以返回200(OK)或者204(No Content)。某些情況下,可能無(wú)法更新指定的資源,這時(shí)可以考慮返回HTTP狀態(tài)碼409(Conflict)。
可以考慮提供一個(gè)批量更新PUT方法,在方法體里放上要更新的所有資源內(nèi)容,并使用URI指明要更新的資源集合。這樣能降低網(wǎng)絡(luò)開(kāi)銷和提高性能。
PATCH方法
使用PATCH請(qǐng)求時(shí),客戶端使用一種 補(bǔ)丁文件 的格式,向已經(jīng)存在的數(shù)據(jù)資源發(fā)送更新請(qǐng)求。服務(wù)端會(huì)使用這個(gè)補(bǔ)丁文件處理更新。補(bǔ)丁文件并不需要描述資源的所有內(nèi)容,只需要告訴更新哪些部分即可。PATCH方法的規(guī)范 (RFC 5789)并沒(méi)有定義這個(gè)補(bǔ)丁文件要有什么樣的特定格式,格式需要根據(jù)請(qǐng)求的media type而定。
JSON應(yīng)該是web API屆最通用的數(shù)據(jù)格式了。而patch方法用到的基于JSON的補(bǔ)丁文件格式有兩個(gè),叫做JSON patch和JSON merge patch。
JSON merge patch相對(duì)簡(jiǎn)單一點(diǎn)。它的結(jié)構(gòu)就像是原始資源對(duì)象的JSON形式內(nèi)容,只是它包含的僅僅是需要更新或者添加的數(shù)據(jù)字段集合,在補(bǔ)丁文件中通過(guò)將字段指定為 null
甚至還可以刪除字段(不過(guò)不適用于原始資源可以顯式的包含'null'值的情況)
舉個(gè)例子,如果原始資源對(duì)象的JSON形式內(nèi)容為:
{
"name":"gizmo",
"category":"widgets",
"color":"blue",
"price":10
}
可能對(duì)應(yīng)的更新文件的內(nèi)容會(huì)是這樣:
{
"price":12,
"color":null,
"size":"small"
}
這個(gè)文件告訴服務(wù)端,要更新 price
,刪掉 color
,并添加 size
字段,同時(shí) name
和 category
字段沒(méi)有任何改動(dòng)。有關(guān)JSON merge patch的更多具體內(nèi)容可以參考 RFC 7396。它對(duì)應(yīng)的media type為 application/merge-patch+json
。
Merge patch并不適用于原本資源中包含顯式'null'值的情況,因?yàn)檠a(bǔ)丁文件中的null有特殊含義(代表刪除)。并且補(bǔ)丁文件不能指定更新被執(zhí)行的順序,不過(guò)這個(gè)有沒(méi)有具體影響要看數(shù)據(jù)對(duì)象的處理邏輯。在RFC 6902中定義的JSON patch,相對(duì)就靈活一些。它可以指明具體要執(zhí)行的操作序列,操作的類型可以是增加、刪除、替換、拷貝和測(cè)試(用來(lái)驗(yàn)證特定值)。它的media type是 application/json-patch+json
。
下表列出了一些處理PATCH請(qǐng)求時(shí)可能會(huì)遇到的一些典型錯(cuò)誤條件,和對(duì)應(yīng)適當(dāng)?shù)腍TTP狀態(tài)碼。
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方法
如果一個(gè)刪除請(qǐng)求被成功執(zhí)行,web服務(wù)端應(yīng)該返回一個(gè)204狀態(tài)碼(No Content),這代表著執(zhí)行已經(jīng)成功,響應(yīng)報(bào)文不需要有任何內(nèi)容。如果請(qǐng)求的資源不存在,應(yīng)該得到一個(gè)HTTP 404(Not Found)。
異步操作
有時(shí)一個(gè)POST、PUT、PATCH或者DELETE操作可能會(huì)需要執(zhí)行一段時(shí)間,如果在成功相應(yīng)之前一直等待,過(guò)長(zhǎng)的延遲可能不太好接受。這時(shí)可以考慮將操作改成異步來(lái)執(zhí)行。返回一個(gè)202(Accepted)狀態(tài)碼來(lái)表明請(qǐng)求已經(jīng)接受并在處理中,但是還沒(méi)有完成。
你應(yīng)該暴露一個(gè)端點(diǎn),來(lái)提供異步請(qǐng)求執(zhí)行狀態(tài)的查詢,這樣客戶端就能輪詢它來(lái)監(jiān)測(cè)狀態(tài)。可以在202報(bào)文頭中包含這個(gè)狀態(tài)端點(diǎn)的地址,如:
HTTP/1.1 202 Accepted
Location: /api/status/12345
如果客戶端向該端點(diǎn)發(fā)起了一個(gè)GET請(qǐng)求,響應(yīng)報(bào)文中應(yīng)該包含操作的執(zhí)行狀態(tài)。另外,也可以提供其他一些信息,比如預(yù)計(jì)要消耗的時(shí)間和一個(gè)可以取消操作的鏈接。
HTTP/1.1 200 OK
Content-Type: application/json
{
"status":"In progress",
"link": { "rel":"cancel", "method":"delete", "href":"/api/status/12345" }
}
如果異步操作創(chuàng)建了新的資源,那么這個(gè)狀態(tài)端點(diǎn)應(yīng)該在操作執(zhí)行完成后返回一個(gè)303(See Other),并在響應(yīng)報(bào)文頭信息中提供新資源的訪問(wèn)地址。
HTTP/1.1 303 See Other
Location: /api/orders/12345
更多相關(guān)信息,可參考 Asynchronous Request-Reply pattern.
數(shù)據(jù)過(guò)濾和分頁(yè)
提供一個(gè)可以查詢資源集合的URI,可能會(huì)出現(xiàn)客戶端其實(shí)只需要一小部分?jǐn)?shù)據(jù),但卻發(fā)起了一個(gè)很大數(shù)據(jù)量的請(qǐng)求。比如,客戶程序需要查詢所有成本大于某個(gè)值的所有訂單,那么可能它會(huì)先請(qǐng)求 /orders 這個(gè)URI拿到所有訂單數(shù)據(jù),然后在客戶端把這些數(shù)據(jù)作一下過(guò)濾。很顯然這樣處理非常的不效率,而且浪費(fèi)了貸款和服務(wù)器算力。
相對(duì)的,這個(gè)API也可以提供請(qǐng)求URI時(shí)攜帶查詢字符串的支持,比如 /orders?minCost=n
。然后服務(wù)端解析處理這個(gè) minCost
查詢項(xiàng),并返回過(guò)濾后的結(jié)果。
一個(gè)對(duì)資源集合的GET請(qǐng)求總是有可能會(huì)返回一大堆數(shù)據(jù),所以最好在你的web API中提供對(duì)單次請(qǐng)求返回結(jié)果數(shù)量的限制。可以考慮提供一個(gè)最大數(shù)量請(qǐng)求查詢參數(shù),來(lái)指明能得到的最大結(jié)果數(shù)量,同時(shí)需要提供一個(gè)偏移量參數(shù),例如:
/orders?limit=25&offset=50
同時(shí)為了避免可能受到的拒絕服務(wù)攻擊,要對(duì)這個(gè)最大數(shù)量參數(shù)設(shè)置個(gè)上限。另外,為了協(xié)助客戶應(yīng)用程序完成分頁(yè)配置,服務(wù)端對(duì)GET請(qǐng)求返回分頁(yè)數(shù)據(jù)時(shí),要在響應(yīng)報(bào)文中包含一些元數(shù)據(jù),用來(lái)標(biāo)明資源可用的總頁(yè)數(shù)等。
類似的,還可以提供一個(gè)sort參數(shù)用來(lái)對(duì)請(qǐng)求數(shù)據(jù)排序,可以使用結(jié)果字段中的任意一個(gè),比如 /orders?sort=ProductID
。不過(guò)這種做法有一個(gè)壞處,因?yàn)橛行┚彺鎸?shí)現(xiàn)用查詢字符串作為緩存的key,sort參數(shù)的調(diào)整可能會(huì)讓之前的緩存失效。
在查詢數(shù)據(jù)的字段數(shù)量特別多時(shí),還可以對(duì)返回字段作一下限制。提供一個(gè)支持逗號(hào)分隔的查詢字符串參數(shù)來(lái)表示需要的字段列表,比如 /orders?fields=ProductID,Quantity
。
對(duì)每一個(gè)支持的查詢字符串參數(shù),提供一個(gè)有意義的默認(rèn)值。例如,如果支持分頁(yè),最好將數(shù)據(jù)條數(shù)默認(rèn)為10,偏移量(頁(yè)碼)默認(rèn)為0。如果支持排序,將sort列默認(rèn)為資源的主鍵,如果支持投影(可選查詢列),將參數(shù)默認(rèn)為所有列。
大型二進(jìn)制文件的部分響應(yīng)
有些資源可能包含二進(jìn)制字段,比如圖片和文件。這些內(nèi)容在不穩(wěn)定網(wǎng)絡(luò)環(huán)境下傳輸可能會(huì)出現(xiàn)問(wèn)題,比如連接中斷,或者處理時(shí)間過(guò)長(zhǎng),為了克服這些問(wèn)題,可以考慮分塊獲取。首先,API需要對(duì)GET請(qǐng)求中的Accept-Ranges這個(gè)用來(lái)請(qǐng)求大文件資源的消息頭提供支持,這個(gè)消息頭表明GET操作支持部分請(qǐng)求,客戶端可以發(fā)起一個(gè)指定了字節(jié)數(shù)范圍的GET請(qǐng)求,來(lái)得到一部分資源數(shù)據(jù)。
同時(shí),最好也實(shí)現(xiàn)對(duì)這些資源的HTTP HEAD請(qǐng)求處理。HEAD請(qǐng)求類似于GET,只不過(guò)它只返回描述資源的HTTP消息頭,而消息體是空的。客戶程序可以發(fā)出HEAD請(qǐng)求來(lái)判斷是否需要使用GET部分請(qǐng)求方式獲取資源。例如:
HEAD https://adventure-works.com/products/10?fields=productImage HTTP/1.1
對(duì)應(yīng)的響應(yīng)報(bào)文為:
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 4580
Content-Length
報(bào)文頭給出了資源的總大小,而 Accept-Ranges
報(bào)文頭則表明對(duì)應(yīng)的GET操作支持部分結(jié)果的請(qǐng)求。有了這些信息,客戶程序便可以把圖片分成小塊獲取。第一個(gè)請(qǐng)求使用了 Range
頭獲取了2500個(gè)字節(jié):
GET https://adventure-works.com/products/10?fields=productImage HTTP/1.1
Range: bytes=0-2499
響應(yīng)報(bào)文使用HTTP狀態(tài)碼206來(lái)表明這是完整內(nèi)容的一部分。Content-Length
報(bào)文頭說(shuō)明了消息體內(nèi)的實(shí)際字節(jié)數(shù)(而非資源的完整大小),Content-Range
報(bào)文頭則表明這是整個(gè)資源的哪一部分(完整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
[...]
客戶程序可以發(fā)起后續(xù)請(qǐng)求來(lái)獲得資源的剩余部分。
使用資源導(dǎo)航 (HATEOAS)
REST有一個(gè)推崇的概念,應(yīng)當(dāng)在不需要先了解URI方案的情況下,做到對(duì)整個(gè)資源集的導(dǎo)航。每一個(gè)HTTP GET請(qǐng)求的響應(yīng)報(bào)文,都應(yīng)該包含一系列超鏈接信息,用來(lái)獲取跟請(qǐng)求對(duì)象有直接關(guān)系的資源,并提供這些資源上的可用操作說(shuō)明。這個(gè)原則被稱為 HATEOAS ,即 Hypertext as the Engine of Application State
。這個(gè)系統(tǒng)實(shí)際上是一種有限狀態(tài)機(jī),請(qǐng)求的響應(yīng)報(bào)文包含了從一種狀態(tài)轉(zhuǎn)移到另一種狀態(tài)的必要信息,跟狀態(tài)轉(zhuǎn)換無(wú)關(guān)的內(nèi)容則不應(yīng)包含在內(nèi)。
當(dāng)前并沒(méi)有針對(duì)HATEOAS原則的通用建模標(biāo)準(zhǔn),本節(jié)示例只展示了一種特定用途下的方案。
比如,處理訂單和客戶關(guān)系時(shí),可以在訂單的表現(xiàn)上包含一些鏈接,用來(lái)標(biāo)識(shí)跟這個(gè)訂單相關(guān)客戶的一些可用操作。比如:
{
"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":[]
}]
}
在這個(gè)例子里,links這個(gè)數(shù)組節(jié)點(diǎn)包含了一個(gè)鏈接數(shù)據(jù)集合,每個(gè)鏈接數(shù)據(jù)都代表了一個(gè)相關(guān)實(shí)體的可用操作。每個(gè)鏈接數(shù)據(jù)又包含了關(guān)系("customer")、URI(https://adventure-works.com/customers/3)、HTTP方法,和可用的MIME類型。這些信息足夠一個(gè)客戶程序用來(lái)執(zhí)行操作了。
鏈接數(shù)組里還包含了一些自引用信息,跟原本獲取到的請(qǐng)求資源有關(guān),它們的關(guān)系被標(biāo)記為 'self'。
根據(jù)不同的資源狀態(tài),links數(shù)組節(jié)點(diǎn)中的內(nèi)容可能會(huì)有所不同。這也正是超文本作為 "engine of application state." 的含義。
RESTful web API的版本控制
通常web API不會(huì)一成不變,隨著業(yè)務(wù)需求變化,會(huì)有更多資源集合出現(xiàn),資源之間的關(guān)系會(huì)變化,資源的數(shù)據(jù)結(jié)構(gòu)也會(huì)有所改變。更新web API來(lái)處理變化的需求或許沒(méi)什么難度,但是必須得考慮這些改動(dòng)對(duì)于那些正在使用這些API的客戶端程序的影響。設(shè)計(jì)和維護(hù)這些web API的程序員,對(duì)這些接口有完全的控制權(quán),但是對(duì)那些客戶端程序可并不一定是這樣,有可能開(kāi)發(fā)它們的是一些遠(yuǎn)處的第三方。所以關(guān)鍵的是能讓現(xiàn)存的客戶端程序繼續(xù)正常運(yùn)行,同時(shí)新的客戶端程序可以充分使用這些新功能和資源。
版本控制可以使web API表明它公開(kāi)的功能和資源,同時(shí)能讓客戶端程序向功能和資源的某個(gè)特定版本發(fā)起請(qǐng)求。下面幾節(jié)描述了不同的版本控制方法,各有其優(yōu)劣。
無(wú)版本控制
這是最簡(jiǎn)單的方法,一些內(nèi)部調(diào)用API可以接受這么做。重大的改動(dòng)一般體現(xiàn)為新的資源或新鏈接,對(duì)已經(jīng)存在的資源添加內(nèi)容一般也不會(huì)有什么問(wèn)題,因?yàn)檎?qǐng)求端程序遇到期望之外的數(shù)據(jù)內(nèi)容,一般會(huì)自動(dòng)忽略掉。
比如,向URI https://adventure-works.com/customers/3
發(fā)起請(qǐng)求,會(huì)返回一個(gè)包含了id、name和address字段的單個(gè)客戶明細(xì)數(shù)據(jù),這些數(shù)據(jù)符合請(qǐng)求端程序的預(yù)期:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}
簡(jiǎn)單起見(jiàn),本節(jié)示例響應(yīng)報(bào)文并沒(méi)有包含
HATEOAS
鏈接.
如果向客戶資源的結(jié)構(gòu)中添加了 DateCreated
字段,響應(yīng)內(nèi)容會(huì)變?yōu)椋?/p>
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"}
已經(jīng)存在的客戶程序會(huì)繼續(xù)正常運(yùn)行,只要它們可以忽略掉不認(rèn)識(shí)的字段,而新的客戶程序可以設(shè)計(jì)為能夠處理這個(gè)新添加的字段。不過(guò),如果資源的結(jié)構(gòu)發(fā)生了更徹底的改動(dòng)(比如刪除或者重命名了某些字段),或者資源間的關(guān)系發(fā)生了變化,那就有可能使得這些正在運(yùn)行的客戶程序無(wú)法再正確運(yùn)轉(zhuǎn)。這種情況下,你就得考慮下面的這些方法了。
URI 版本控制
每當(dāng)對(duì)web API或者資源結(jié)構(gòu)進(jìn)行改動(dòng)時(shí),可以對(duì)每個(gè)資源的URI添加一個(gè)版本號(hào),先前的URI應(yīng)繼續(xù)像之前那樣運(yùn)作,返回原本的資源數(shù)據(jù)。
對(duì)前面的例子作下擴(kuò)展,如果將 address 字段重構(gòu)為包含不同組成部分的子字段(比如街道、城市、省份和郵政編碼),這個(gè)版本的資源可以發(fā)布為一個(gè)包含了版本號(hào)的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}}
這種版本控制機(jī)制非常簡(jiǎn)單,但是它依賴于服務(wù)器能將請(qǐng)求路由到正確的服務(wù)端點(diǎn)。而且,隨著web API的逐步迭代和服務(wù)器提供支持的版本數(shù)量增多,它會(huì)變得很笨重。另外,從純粹主義角度去看,返回了同樣數(shù)據(jù)(客戶3)的URI,它們的版本不應(yīng)該是不一樣的。這個(gè)方案還會(huì)讓 HATEOAS
的實(shí)現(xiàn)更加復(fù)雜,因?yàn)樗墟溄佣疾坏貌粚姹咎?hào)包含在它們的URI中。
查詢字符串版本控制
相比于提供多個(gè)不通的URI,更好的做法可能是在HTTP請(qǐng)求上附帶一個(gè)指定了資源版本的查詢字符串參數(shù),比如 https://adventure-works.com/customers/3?version=2
。給這個(gè)版本參數(shù)一個(gè)有意義的默認(rèn)值,比如1,這樣舊的客戶程序可以忽略掉它而不受到影響。
這種做法的好處是,在語(yǔ)義上,同樣的資源請(qǐng)求用了相同的URI,不過(guò)這需要相應(yīng)的代碼正確的解析請(qǐng)求中的查詢字符串,并給與正確的HTTP響應(yīng)。另外此方法同樣會(huì)遇到跟URI版本控制一樣的問(wèn)題,即實(shí)現(xiàn) HATEOAS
會(huì)比較麻煩。
一些舊版瀏覽器和網(wǎng)絡(luò)代理不會(huì)緩存包含查詢字符串的請(qǐng)求響應(yīng)內(nèi)容,在其上運(yùn)行的網(wǎng)絡(luò)程序調(diào)用web API時(shí)的性能可能會(huì)受到影響。
消息頭版本控制
除了將版本號(hào)放到查詢字符串參數(shù)中,還可以實(shí)現(xiàn)一個(gè)自定義消息頭來(lái)指明需要的資源版本。這個(gè)做法需要客戶端程序在所有請(qǐng)求上攜帶正確的消息頭,盡管服務(wù)代碼可以在請(qǐng)求忽略了這個(gè)消息頭時(shí)給一個(gè)默認(rèn)值(比如版本1)。下面這個(gè)例子使用了名為 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}}
和前面兩種版本控制方案一樣,實(shí)現(xiàn) HATEOAS
也需要在所有鏈接中帶上正確的消息頭。
媒體類型版本控制
在前面有介紹過(guò),當(dāng)客戶端程序發(fā)出一個(gè)HTTP GET請(qǐng)求到web服務(wù)端時(shí),它應(yīng)當(dāng)使用 Accept
消息頭明確它可以處理的內(nèi)容格式。Accept
消息頭經(jīng)常用來(lái)指明客戶端程序希望得到的響應(yīng)報(bào)文格式應(yīng)該是XML、JSON或者其他一些通用格式。不過(guò),也可以擴(kuò)展一些自定義媒體類型,使得客戶程序可以在其中加一些信息來(lái)指明需要的資源版本。
下面這個(gè)例子在 Accept
消息頭中指定了 application/vnd.adventure-works.v1+json
,其中的 vnd.adventure-works.v1
就向web服務(wù)端表明了它需要的是1這個(gè)資源版本,并希望得到j(luò)son格式的響應(yīng)內(nèi)容:
GET https://adventure-works.com/customers/3 HTTP/1.1
Accept: application/vnd.adventure-works.v1+json
處理請(qǐng)求的代碼需要處理這個(gè)消息頭,并盡可能的滿足它(客戶端程序可能會(huì)在 Accept
中指定多個(gè)格式,服務(wù)端只需要選擇其中最合適的一個(gè)作為響應(yīng)格式即可)。Web服務(wù)端在響應(yīng)體中使用 Content-Type
報(bào)文頭來(lái)對(duì)數(shù)據(jù)格式進(jìn)行確認(rèn):
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
消息頭中指定了無(wú)法處理的媒體類型,服務(wù)端可以返回HTTP狀態(tài)碼406(Not Acceptable),或者使用一個(gè)默認(rèn)類型來(lái)處理。
這種做法被認(rèn)為是最純粹的版本控制方案了(譯者猜應(yīng)該是說(shuō)這種做法可以在連接上只是明確指向資源即可,不需要做其他調(diào)整,URI版本方案,和查詢字符串版本方案,都會(huì)影響這個(gè)URI的干凈度),并且它原生支持 HATEOAS
,因?yàn)榭梢栽阪溄庸?jié)點(diǎn)中指明 MIME 類型。
在選擇一種版本控制方案時(shí),需要同時(shí)考慮到對(duì)性能的潛在影響,特別是在web服務(wù)端的緩存處理。URI和查詢字符串版本控制方案是可以緩存的,因?yàn)閿y帶了版本信息的完整URI和查詢字符串會(huì)作為緩存的唯一標(biāo)識(shí),同樣的版本請(qǐng)求每次都指向同樣的數(shù)據(jù)。
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.(這段譯者未能完全理解,猜其意思,可能跟代理的緩存策略有關(guān),因?yàn)镠eader和MediaType的版本控制方案,在請(qǐng)求不同版本時(shí)的URI和query string并沒(méi)有變化,代理可能會(huì)使用不正確的緩存數(shù)據(jù)直接返回,而不是向web服務(wù)端發(fā)起新的請(qǐng)求來(lái)獲取數(shù)據(jù))
Open API 計(jì)劃
Open API 計(jì)劃是由一個(gè)行業(yè)聯(lián)盟創(chuàng)建的,目的是標(biāo)準(zhǔn)化供應(yīng)商之間的REST API描述。作為這一計(jì)劃的一部分,Swagger 2.0規(guī)范被重新命名為OpenAPI規(guī)范(OAS),并納入了Open API 計(jì)劃。
如果你想要把自己的web API改造為OpenAPI,可以參考下面幾點(diǎn):
OpenAPI規(guī)范附帶了一套關(guān)于REST API應(yīng)該如何設(shè)計(jì)的固執(zhí)的指導(dǎo)方針。這對(duì)互操作性有好處,但在設(shè)計(jì)API以符合規(guī)范時(shí)需要更加小心。
OpenAPI提倡契約優(yōu)先的方法,而不是實(shí)現(xiàn)優(yōu)先的方法。契約優(yōu)先意味著首先設(shè)計(jì)API契約(接口),然后編寫(xiě)實(shí)現(xiàn)該契約的代碼。
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.