1. 程式人生 > >優秀REST風格 API的設計原則

優秀REST風格 API的設計原則

本文由逍遙子翻譯自:https://codeplanet.io/principles-good-restful-api-design/

文中註釋由逍遙子根據個人理解填寫,轉載時請附帶本文的連線。

設計優秀的REST風格API非常困難!API是服務提供方和使用方之間的契約,打破該契約將會給服務端開發人員招來非常大的麻煩,這些麻煩來自於使用API的開發人員,因為對API的改動會導致他們的移動app無法工作。一個好的文件對於解決這些事情能起到事半功倍的作用,但是絕對多數程式設計師都不喜歡寫文件。

如果想讓服務端的價值更好的體現出來,就要好好設計API。通過這些API,你的服務/核心程式將有可能成為其他專案所依賴的平臺;目前的大公司:Facebook、Twitter、Google、Github、Amazon、Netflix等等無不依賴API,如果沒有精心設計的API對外開發它們的資料,這些公司也就不會像今天這麼強大。事實上,整個產業存在的目的就是消費上述平臺提供的資料。

你提供的API越易用,就會有越多人願意使用它。

如果在設計API時能遵循本文件提出的原則,那麼你設計出的API就能讓呼叫方更容易理解和使用,也能大幅減少呼叫方對你的抱怨;我已將文件內容按主題分別進行詳細描述,讀者可選擇自己感興趣的主題,而無需順序閱讀。

本文件中所使用的術語及其含義如下:

  • Resource(資源):單個例項物件,例如animal(一隻動物);[逍遙子筆記:Roy ThomasFielding(REST風格的提出者)如此解釋資源的含義:一個資源可以是一份文件、一張圖片、一個與時間相關的服務(例如:洛杉磯今天的天氣)等等,資源是實體的概念性對映,而不是實體本身;個人理解:一張圖片是一個資源,但是在我本機中有一個的名字叫做“jason.txt”的、實實在在的檔案實體,就不能稱作資源,它是資源實體,簡單而言:“資源”與“資源實體”之間猶如類和物件的關係。]
  •   Collection(集合):集合是一組同類的例項物件,例如animals(一群動物)。
  •   HTTP:一種在網路上傳輸的通訊協議;
  •   Consumer(使用方、使用者):能夠傳送http請求的客戶端程式;[逍遙子筆記:這裡Consumer實際是指API的呼叫方,如果直譯成消費者更讓人困惑]
  •   Third Party Developer(第三方開發人員):不是你專案專案團隊的成員,但希望使用你的服務的那些開發人員;
  •   Server(伺服器):能夠被Consumer通過網路訪問的HTTP伺服器/程式;
  •   Endpoint(端點):伺服器提供的一個URL,它標識了一個資源(Resource)或者集合(Collection);
  •   Idempotent(冪等):多次重複操作得到的結果一樣;
  •   URL Segment(URL片段):從某個URL中取出的一小部分片段;

資料設計和抽象

規劃API的展示形式可能比你想象的要簡單,首先要確定你的資料是如何設計以及核心程式是如何工作的?在新開發專案中進行API設計會比較容易,如果要對一個已經存在的專案進行修改使之符合REST風格,那麼你就需要在抽象方面多下功夫了。[逍遙子筆記:按照RoyThomas Fielding對REST的設計,REST風格更適用於以資料為中心的架構,而非以計算為中心的架構]

有時,集合(Collection)可以表示資料庫裡的一張表,資源(Resource)表示表中的一行。[逍遙子筆記:這裡的比喻感覺有些不恰當,資料庫的一行對應的是一個資源實體,而非資源!]但是多數情況不是這樣簡單。事實上,你的API應該是對資料和業務邏輯的“儘可能的”抽象。非常重要的一點是:複雜的應用資料將會讓第三方開發人員理解和使用起來非常困難,如果你這麼做了,他們就不想使用你的API了。[逍遙子筆記:設計API時,引數和返回值的資料不應太複雜,否則開發人員發起呼叫和處理返回結果時都要處理半天,非常麻煩!]

有些情況下,服務的資料不能通過API暴露出來。一個常見的例子就是許多API都不允許第三方開發人員建立使用者。[逍遙子筆記:設計API時,需要提供什麼功能時就提供什麼API出來,不要過早、過多暴露不必要的API介面,我們開發過程中通常會遇到這種情況:無論能否用到,先把自己服務的所有功能暴露出來再說,說不定就用上了,到時候就省得再修改了!]

動作(Verbs)

你肯定知道HTTP的GET和POST請求,這是兩個通過瀏覽器訪問各種網頁時最常用的請求。術語POST甚至變成一個人們的常用語,即使不知道網際網路如何工作的使用者也知道能POST資訊到朋友的Facebook留言板裡。

你需要了解這裡列出的4.5個非常重要的HTTP動作,這裡的0.5個是指PATCH,因為它在功能上與PUT非常類似,剩下4個通常被API開發人員兩兩結合使用[逍遙子筆記:例如GET和POST,PUT和DELETE]。這裡是這些動作以及它們對應的資料庫呼叫(我認為大多開發人員更熟悉資料庫操作而不是設計API)。[逍遙子筆記:正式基於作者的這個理解,所以本文的很多地方都是用資料庫來解釋API,其實這個也非常恰當,因為REST架構風格是基於資源的,而資料庫也是資源的一種形式。下面這些解釋中,括號裡的內容就是與該HTTP動詞類似的資料庫操作]

  •   GET(SELECT):從伺服器獲取一個指定資源或一個資源集合;
  •   POST(CREATE):在伺服器上建立一個資源;
  •   PUT(UPDATE):更新伺服器上的一個資源,需要提供整個資源;
  •   PATCH(UPDATE):更新伺服器上的一個資源,只提供資源中改變的那部分屬性;
  •   DELETE(DELETE):移除伺服器上的一個資源;

還有兩個不常見的HTTP動作:

  •   HEAD – 獲取一個資源的元資料,例如一組hash資料或者資源的最近一次更新時間;
  •   OPTIONS – 獲取當前使用者(Consumer)對資源的訪問許可權;

一個優秀的API將會充分利用這4.5個HTTP動作讓第三方開發人員與自己的資料互動,並且URL中決不包含動作/動詞。[逍遙子筆記:URL是對資源描述的抽象,資源的描述一定是名詞,如果引入了動詞,那這個URL就表示了一個動作,而非一個資源,這樣就偏離了REST的設計思想]

通常,GET請求能夠被瀏覽器快取(而且通常都會這麼做),例如,當用戶發起第二次POST請求時,快取的GET請求(依賴於快取首部)能夠加快使用者的訪問速度。一個HEAD請求基本上就是一個沒有返回體的GET請求,因此也能被快取。

版本控制

無論你在設計什麼系統,也不管你事先做了多麼詳盡的計劃,隨著時間的推移和業務的發展,你的程式總會發生變化,資料關係也會發生變化,資源可能會被新增或者刪除一些屬性。只要軟體還在生存期內並且還有人在用它,開發人員就得面對這些問題,對於API設計來說,尤其如此。[逍遙子筆記:根據RoyThomas Fielding對資源的解釋:資源描述和資源實體是分開的,而設計REST API是基於資源描述,當資源實體發生變更時,只要修改資源描述和資源實體的對映,就能保證資源描述不變,進而保證所設計的API不變,所有使用API的第三方程式也不需要做任何修改,因此,REST風格就是用於解耦這種服務端和客戶端的關係]

API是一份呼叫方(Consumer)和伺服器之間已達成的契約,更改伺服器的API必然會影響其向後相容性,對契約的破壞將會招致使用方(Consumer)的抱怨,如果你改動很大,他們可能會放棄使用你的服務。為了確保伺服器程式能夠進化升級,同時能夠讓使用方感到滿意,你需要在引入新版本API的同時繼續讓舊版本的API正常工作。

注意,如果你只是簡單地為API增加一些新特性,例如為資源增加新屬性(這些新屬性並非必須的,沒有它們資源也能工作),或者新增了端點,那就不需要升級API的版本號,因為這些變化並不會破壞向後的相容性。當然,你還是需要更新API設計文件(你和呼叫方的契約)。[逍遙子筆記:文件對於API來說太重要了,沒有好的文件必然沒有好的API,因此API和它的文件一定同步修改,甚至要先修改文件]

過一段時間之後,你可以告訴呼叫方不建議(deprecate)使用舊版本[逍遙子筆記:就像java裡面的depredated註釋一樣,用於告訴使用方,我不建議你使用它了,過一段時間之後我可能就不支援它了]。不建議使用一個API並不意味著馬上就要關閉它或者降低它的服務質量,而是告訴你的API使用人員他們需要版本升級,舊的版本將在未來一段時間之後被停止服務。[逍遙子筆記:通過過渡期來提醒使用者升級API,明確告訴呼叫方什麼API處在過渡期,過渡期內新老版本同時都能工作,但老版本在過渡期之後就會被去掉]

在URL中加入版本號是一個優秀的API設計,當然還有另一個常用的解決辦法就是把版本號放在請求首部中[逍遙子筆記:HTTP請求的accept欄位],根據多年與第三方開發人員打交道的經驗,我可以告訴你把版本號放在URL裡要比放在請求的首部中更容易實現和使用。[逍遙子筆記:對這種方法還有一些疑問,個人理解,版本應該標識資源,也就是版本是針對URL尾部的端點,例如https://api.example.com/vi/zoos中的/zoos,按照這種思路,當一個專案包含很多型別的資源,這些資源需要要分級進行展示,此時版本號放在URL裡可能遇到很多問題,例如:有個即時通訊專案IM,它包含user_info(使用者資訊),user_rel(使用者關係),message三個資源大類,其中user_info又分為公開資訊public和私有資訊private兩個型別,public型別包括個人簡介的二維碼資訊self_info,private型別包括個人的應用鎖資訊lock, user_rel(使用者關係)型別包括好友關係friends和群組groups兩個型別,groups包含群成員member和群資訊info兩個子型別資源,messge包含新聊天訊息new和歷史聊天訊息history,這些資源組織成樹狀結構後如下圖所示:


假設我的URL根(在本文後面將會介紹URL的根)為:https://test.jason.com/im/*,那麼上述資源對應的URL為:

(1)https://test.jason.com/im/user_info/public/self_info 

該URL表示資源:使用者個人簡介的二維碼資訊;

(2)https://test.jason.com/im/user_info/private/lock

該URL表示資源:使用者的應用鎖資訊;

(3)https://test.jason.com/im/user_rel/friends

該URL表示資源:使用者的好友資訊

(4)https://test.jason.com/im/user_rel/groups/member

該URL表示資源:群組的群成員

(5)https://test.jason.com/im/user_rel/groups/info

該URL表示資源:群組的資訊

(6)https://test.jason.com/im/message/new

該URL表示資源:使用者新聊天訊息

(7)https://test.jason.com/im/message/history

該URL表示資源:使用者的歷史聊天訊息

問題是:在這種情況下版本號應該放在URL的那個地方?

如果版本號是針對IM整個專案,例如這裡的IM專案整體分為v1和v2兩個大版本,此時的URL首部就能改成:https://test.jason.com/im/v1/*,https://test.jason.com/im/v2/*,實際的URL資源將變成:https://test.jason.com/im/v1/message/new。

如果版本號是針對IM專案中的某個子類,例如這裡的message型別分為v1和v2兩個版本,那麼訊息類的URL根就會變成:https://test.jason.com/im/message/v1/*,https://test.jason.com/im/message/v2/*,實際的URL資源將會變成:https://test.jason.com/im/message/v1/new。

如果版本號針對的資源型別更詳細,那麼版本號就會更靠後。在一些負責型別的專案中,資源的型別也會非常複雜,層級也更深,按照本文作者建議的方法就很難確定v1的位置]

分析

跟蹤各版本/端點的API被呼叫的情況[逍遙子筆記:由此可見版本號對應URL末尾的“端點”]。可以通過在資料庫中為每個API增加一個計數器來實現,來一個請求就將對應的使用計數加1。統計各API的呼叫情況會帶來很多好處,例如,優化呼叫頻度最高的API。

為了構建第三方開發人員喜歡的API,最重要事情是確定何時不建議(deprecate)使用者使用舊版本API,你可以使用這些不建議的(deprecated)API來告知第三方開發人員,這是在你廢掉舊版本之前提醒他們的一個好途徑。

通知第三方開發人員的過程可以自動化完成,例如,每呼叫10,000次deprecated API就給相應開發人員發一個郵件提醒。

API的根URL

無論你是否相信,API的根設計非常重要。當開發人員接手一個使用你的API所開發的舊專案,並且需要為它增加新特性的時候,他可能完全不瞭解你的服務,或許他們知道的就是所呼叫的一系列URL。重要的是你API的URL根應該儘可能簡單,一個又長又複雜的URL看起來就嚇人,它很可能就把這些第三方開發人員嚇跑了。

這裡有兩個普通的URL根:

  •   https//example.org/api/vi/*
  •   https//example.com/vi/*

如果你的應用程式很龐大,或者未來它可能變得很龐大,你可以把API放在各自的子域內,這麼做可以讓你的程式在以後的發展中更靈活、更容易擴充套件。[逍遙子筆記:這裡作者想表達的意思好像是把URL所表示的資源進行分類、分層級,不同的資源放在不同的類中]

如果你的程式不會變得這麼大,或者你想簡化程式的使用(例如,你想通過一個框架同時提供網站和API服務),就把你的API放在URL的根域(例如:/api/)之後。

最好讓你API的根也包含內容。例如,訪問githubAPI的根就會得到一個端點(端點代表資源)列表。我更偏好使用根URL獲取那些對“正在迷茫中的”開發人員來說有用的資訊,例如:怎麼獲取API的開發者文件。

注意使用HTTPS字首,一個好的RESTfulAPI必須使用HTTPS作為字首。[逍遙子筆記:例如:https://api.example.com/v1/zoos]

端點

端點是URL中用於標識一個特定資源或資源集合的那部分URL片段。

假如你想構建用於表示多個動物園資源的API,其中,每個動物園都包含很多動物(每個動物只能屬於一個動物園),顧員(他們可以在多個動物園工作),並且需要跟蹤每個動物的詳細資訊,那麼這些API的端點可能如下所示:[逍遙子筆記:下列URL中紅色加粗的字尾就是端點]

在介紹這些端點的作用時,你需要給出這些端點以及操作它們的HTTP動作。例如下面給出的是剛才所構建動物園API列表的功能,注意,我在每個端點前面都加上了HTTP動作,就像HTTP請求中所使用的那樣。

  •   GET    /zoos:列出所有的動物園(包括動物園的ID、名稱以及簡介);
  •   POST   /zoos: 建立一個新的動物園;
  •   GET    /zoos/ZID:獲取一個完整的動物園物件;[逍遙子筆記:這裡的動物園物件僅僅指“動物園”,例如“動物園”的ID、名稱、介紹等資訊,而不包含該動物園中的動物、僱員等下轄型別的資訊]
  •   PUT    /zoos/ZID:更新一個動物物件(包含全部資訊);
  •   PATCH  /zoos/ZID:更新一個動物物件(包含部分資訊);
  •   DELETE /zoos/ZID:          刪除一個動物園物件;
  •   GET    /zoos/ZID/animals:獲取指定動物園的全部動物資訊(包括動物的ID和名稱);
  •   GET    /animals:是指列出所有動物資訊;
  •   POST  /animals: 建立一個新的動物;
  •   GET   /animals/AID: 獲取某個動物的物件資訊;
  •   PUT   /animals/AID:更新一個動物的物件資訊(提供物件的全部內容);
  •   PATCH  /animals/AID:更新一個動物的物件資訊(提供物件的部分內容);
  •   GET    /animal_types: 獲取所有的動物型別資訊;
  •   GET    /animal_types/ATID: 獲取指定動物型別的型別描述資訊;
  •   GET    /employees:獲取所有的僱員列表;
  •   GET    /employees/EID:獲取一個指定僱員的資訊;
  •   GET    /zoos/ZID/employees:獲取指定動物園的全部僱員列表;
  •   POST   /employees:建立一個新僱員;
  •   POST   /zoos/ZID/employees:為指定動物園增加一個僱員;
  •   DELETE /zoos/ZID/employees/EID:解僱指定動物園的某個僱員;

上述列表中,ZID表示動物園的ID,AID表示動物ID,EID表示僱員ID,ATID表示動物型別ID,在文件中給出關鍵詞及含義是一個非常好的習慣。

在上面的例子中我已簡要列出常見API的URL字首,這種簡化方式(省略URL字首)非常有利於溝通,但是在你的API文件當中,還是要使用全部的URL(例如:GET https://api.example.com/vi/animal_type/ATID)。[逍遙子筆記:在非正式文件中介紹一個API時可以採用端點代替完成URL這種方法:HTTP動作+端點+該端點的功能說明,端點要比完整URL短的多,這樣更容易表述,也不影響理解,但是在正式文件中還是要把URL寫全]

這裡需要注意資料之間關係的展示,尤其是僱員和動物園之間的多對多的關係。你可以通過增加URL的方式來表示更多的資料關係[逍遙子筆記:根據RoyThomas Fielding對資源的解釋,關係也是一種資源,因此,在遇到多對多的資料關係時,可以將資料關係進行拆分,併為每個關係都增加一個URL]。當然,這裡並沒有一個HTTP操作能表示解除僱員,但是我們可以通過刪除指定動物園的僱員的方式來達到同樣的效果。[逍遙子筆記:這是對上面“DELETE/zoos/ZID/employees/EID:解僱指定動物園的某個僱員”這個條目的解釋]

過濾器

當用戶請求獲取一組物件列表時,你就需要對結果進行過濾並返回一組嚴格符合使用者要求的物件。有時返回結果的數量可能非常大,但是你也不能隨意對此進行約束,因為這種服務端的隨意約束會造成第三方開發人員的困惑。如果使用者請求了一個集合,並對返回結果進行遍歷,然後只要前100個物件,那麼這裡就需要由使用者來指明這個限制量。這樣使用者就不會有這樣的疑惑:是他們程式的bug還是介面限制了100條?還是網路只允許傳這麼大的包?[逍遙子筆記:在IM專案中有個介面讓使用者拉取自己的歷史訊息,我們就需要在介面中增加一個引數讓使用者來確定本次要拉取多少條歷史訊息,伺服器端根據使用者傳入的限制量來確定返回訊息的條數,而不是由伺服器來在實現時就確定該介面一次呼叫只能返回幾條]

儘量減少對第三方開發人員的隨意約束。[逍遙子筆記:不要在介面中新增預設的約束條件]

非常重要的一點:讓第三方開發人員自己指定排序過濾器/返回結果集的約束條件。這麼做的最重要原因是:使用者能用盡量少的網路消耗盡快獲取到結果;第二個重要原因是:使用者可能很懶,想讓服務端幫他們做好分頁和過濾;還有一個對客戶端不那麼重要,但是對服務端很重要的原因:這麼做會降低請求的資源負載。

過濾器通常用於過濾GET請求返回的資源集合,在GET請求中,可以通過URL傳遞過濾器資訊。你可以放心把下面例子中列出的過濾器型別應用到自己的API中:

  •   ?limit=10:限制返回給使用者的結果集的數量(通常用於分頁);
  •   ?offset=10:給使用者返回一個結果集(通常用於分頁);[逍遙子筆記:指定偏移量,從指定位置開始返回結果集,與limit結合使用就可以達到分頁效果,第一次從開始獲取指定數量的結果,後續都要從上次結果之後開始返回]
  •   ?animal_type_id=1:返回符合條件的結果集(類似於資料庫的WHERE查詢條件:WHERE animal_type_id=1);
  •   ?sortby=name&order=asc:將結果集按照指定屬性和指定排序方式進行排序(類似於資料的ORDER BY name ASC);

上述部分過濾器與前面介紹的某些URL端點功能重複,例如前面提到的URL:GET /zoo/ZID/animals在功能上就與使用過濾器的GET /animals?zoo_id=ZID重複。功能單一的端點對於第三方開發人員來說更容易使用,尤其是他們使用你提供的請求做一些複雜的開發時,更是如此。在API文件中明確寫出這些功能重複的請求方式,將會消除第三方開發人員對這些重複功能的困惑,否則他們就會懷疑這些重複功能之間是否有差異!

還有一點,當需要對資料進行過濾或者排序時,你要給第三方開發人員(Consumer)列出哪些屬效能用於過濾或排序,我們不希望把任何資料庫操作的錯誤返回給使用者。[逍遙子筆記:不要把服務內部的錯誤或者問題暴露給第三方開發人員]

狀態碼

充分利用適當的狀態碼對於設計REST 風格的API來說非常重要,因為HTTP狀態碼已有標準定義,並且各種網路裝置都能讀取並識別這些它們。例如,通過配置負載均衡器的引數來避免將請求發往出現50X錯誤的服務程式[逍遙子筆記:50X表示服務程式內部出錯]。這裡將列出一些可供你選擇使用的HTTP狀態碼,它們可以成為你設計良好返回碼的出發點:

  •   200 OK – [GET]

    客戶端向伺服器請求資料時,伺服器找到這些資料並將之返回給客戶端(此行為冪等);[逍遙子筆記:GET操作只能獲取資料(即只讀),不應該對伺服器的資料進行任何形式的修改]

  •   201 CREATED – [POST/PUT/PATCH]

客戶端向伺服器傳送資料,伺服器為這些資料建立一個資源;

  •   204 NO CONTENT – [DELETE]

客戶端請求伺服器刪除一個資源時,伺服器將該資源刪除;[逍遙子筆記:返回碼204表示執行成功了,但是沒有資料。HTTP 的RFC2616中對於204返回碼的描述為:如果客戶端是個代理(例如瀏覽器),它不應該改變“觸發該請求的”頁面展示,該返回值主要用於輸入行為發生時,雖然新的或更新過的元資料資訊被應用於當前頁面,但代理(瀏覽器)不能改變當前的頁面展示,原文為:

If the client is a user agent, itSHOULD NOT change its document view from that which caused the request to besent. This response is primarily intended to allow input for actions to takeplace without causing a change to the user agent’s active document view,although any new or updated metainformation SHOULD be applied to the documentcurrently in the user agent’s active view.]

  •   400 INVALID REQUEST – [POST/PUT/PATCH]

客戶端給伺服器傳送了一個無效的請求,伺服器對此不作任何動作(此行為冪等)。

  •   404 NOT FOUND – [*]

客戶端請求了一個不存在的資源或資源集合,服務端對此不作任何動作(此行為冪等)。

  •   500 INTERNAL SERVER ERROR – [*]

伺服器內部發生了錯誤,客戶端無法知道請求是否被執行成功了。

狀態碼範圍

1XX的返回碼預留給HTTP的底層使用,在你的整個職業生涯中都不會主動傳送這種返回碼;

2XX的返回碼錶示請求按照預期執行併成功返回了資訊。服務端要儘可能給使用者返回這種結果。

3XX的返回碼錶示請求重定向,大多數API都不會經常使用這種請求(),但是最新的超媒體API會充分使用這些功能。

4XX的返回碼主要表示由客戶端引起的錯誤,例如請求引數錯誤或者訪問一個不存在的資源,這些必須為冪等操作,並且不能改變伺服器的狀態[逍遙子筆記:其實伺服器的狀態發生了改變就意味著操作不是冪等了]

5XX的返回碼主要表示由伺服器引起的錯誤,通常情況下,這些錯誤都是開發人員([逍遙子筆記:這裡應該是伺服器程式的開發人員])接觸不到的底層函式丟擲來,然後傳遞給使用者([逍遙子筆記:這裡應該是第三方開發人員])的。使用者在收到5XX的返回碼時,他們不知道伺服器當前的工作狀態是否正常,因此,要儘量避免這種情況發生。

返回值文件

當第三方開發人員在傳送HTTP請求時,他們需要事先了解這些請求的返回值資訊,下述列表就是一些REST風格API以及它們對應的返回值資訊:[逍遙子筆記:與前面的介紹URL的功能類似,只是這裡將最後面的URL描述換成返回值描述,這裡依然採用三段式描述法:HTTP請求動作+端點+端點對應的返回值描述資訊]

  •   GET /collection:返回一個資源物件的列表;
  •   GET /collection/resource: 返回一個資源物件;
  •   POST /collection:返回新建立的資源物件
  •   PUT /collection/resource:返回一個完整的資源物件;
  •   PATCH /collection/resource:返回一個完整的資源物件;[逍遙子筆記:雖然請求中只帶了部分資源物件的內容,但是返回的內容卻是整個資源物件]
  •   DELETE /collection/resource:返回一個空文件;

需要注意的是:當用戶建立一個資源時,他們通常並不想知道新建立資源的ID(也不想知道其他屬性,例如修改或建立的時間戳)[逍遙子筆記:這一點略有疑惑,這種設計思路應該也是區分場合的,如果資源很複雜,本文介紹的這種思路或許可行,如果資源原本就很簡單,例如我們以前設計的傳送訊息介面,就直接給使用者返回所傳送訊息的ID]。這些新增的屬性資訊可通過後續請求獲得,當然也可以通過初始化POST請求來返回。

授權

多數情況下,伺服器想確切知道每個請求的發起方是誰?當然,部分API介面能放開被用匿名訪問,但通常介面只能被授權的使用者訪問。

OAuth2.0為此提供了一個很好的實現途徑。你能知道每個請求是由哪個客戶端發起?這些請求背後分別代表了哪些使用者?以及提供一種標準化的使用者訪問或撤銷訪問方式,所有這些都無需第三方使用者的登入授信。

還有OAuth1.0和xAuth也能完成類似功能。無論採用那種方法,一定要確保通用性以和良好的文件設計,在文件中對使用者常用語言/平臺的各種不同封裝庫進行詳細說明。[逍遙子筆記:這些服務以庫的方式供客戶端呼叫,例如SDK,因此在文件中要對各種形式的封裝庫進行詳細說明]

我可以如實地告訴你,儘管OAuth1.0a雖然是最安全的選項,但是它非常難以部署。我遇到很多第三方開發人員抱怨他們不得不實現自己的庫,因為OAuth1.0a沒有他們所使用語言對應的庫。我花費了大量的時間用於解決那些難以理解的“invalid signature”錯誤。因此,我建議你使用其他的替代方案。

內容型別

目前,大多數REST風格API介面都提供JSON格式的資料,你所能想起來的Facebook、Twitter、GitHub都是如此。XML方式已經逐漸退出人們的視野(除了一些大公司內部還在使用之外),幸虧SOAP方式已經消失,我們已經看不到返回HTML格式資料的API介面了。

開發者常用的開發語言或框架都能輕易解析你返回的各種有效資料。如果你正在使用不同的序列化器構建一個通用的返回物件,你可以使用前面提到的任意資料格式(SOAP除外),不過在返回資料時需要注意處理請求首部的Accept欄位。[逍遙子筆記:HTTP請求頭部的Accept可用於指定返回資料的格式]

一些API開發人員建議針對不同返回內容的型別,為URL(在端點之後)新增擴充套件欄位,例如:.json,.xml,或者.html,但我不建議這麼做,我建議使用HTTP請求首部的Accept欄位(HTTP的RFC文件中有對Accept的解釋),並且覺得這才是合適的方法。

超媒體API

超媒體API可能代表REST風格API的未來發展,它們在思想上更符合HTTP和HTML的設計初衷。[逍遙子筆記:根據REST作者Roy Thomas Fielding的描述,REST核心是面向資源的設計,超媒體服務所提供了各種多媒體資源的訪問,它在本質上符合了以資源為中心的設計]

在使用非超媒體的REST風格API時,URL端點也是客戶端和伺服器之間契約的一部分,這些端點必須事先告知客戶端,一旦改變它們,客戶端就無法按預期與伺服器進行互動,這其實也是一種約束。

現在,API的使用者不僅僅是能發起HTTP請求的使用者代理,人們更常用瀏覽器發起HTTP請求。然而,使用者不會被這些預先定義的、REST風格API的URL端點所約束。是什麼讓使用者變得如此特殊呢?現在的網頁能讓使用者先讀主題,然後點選他們感興趣的主題所對應的連結,訪問他們想訪問的網站或者想閱讀的內容,此時URL發生變化的時候,使用者不受影響(除非使用者為某個網頁打了標籤,在他們訪問這些打了標籤的網頁時會自動跳轉到主頁,使用者需要再從主頁中尋找他們所感興趣資料的新路徑)。[逍遙子筆記:我們的網站通常採用這種方式,尤其是各種入口網站,例如網易:www.163.com,新浪:www.sina.com,開啟這些入口網站,我們看不到一個URL,我們所看到的都是一個個的主題,每個主題對應一個超連結(URL),當這些主題對應的URL發生變化時,只需要調整主題和超連結(URL)的對映關係即可,使用者實際上看不到這些URL的變化]

超媒體API概念和一個普通人的行為類似。請求API的根目錄將會獲得一個URL列表,這些URL列表的每一項都可能對應了一個資訊集,並且它以使用者能夠理解的方式來描述這些資訊集合。無需為每個資源提供ID(除非特別邀請),因為一個URL就唯一標識了一個資源。

超媒體API使用者在訪問連線並且收集資訊時,返回結果中將被放入最近更新的URL連結,因此URL無需事先與使用者約定。如果URL被快取了,後續請求又返回了404[逍遙子筆記:404表示請求了一個不存在的資源],使用者只需簡單地回到根目錄重新尋找內容即可。

當從集合中檢索一個資源列表時,要給使用者返回這個資源列表的完整URL。當執行POST/PATCH/PUT請求時,可用返回碼為3XX的響應來重定向到新的資源。

JSON即無法給我們提供需要的語義來指明哪些屬性是URL,也不能說明URL怎樣與當前的文件關聯起來;可能正如你猜測的那樣,HTML可以提供這樣的資訊。我們可以先通過API拿到資料,然後再進行HTML處理。想想CSS陪伴我們走過的這些路,我們可能有一天會看到:在獲取同樣URL和內容的時候,無論通過API請求還是網站訪問,都採用同一種處理方式。[逍遙子筆記:JSON提供資料內容,並未提供資料展示功能]

文件

老實說,如果沒有完全按照本文件的指導,你的API也不一定會太差,然而,如果你沒有為API編寫合適的文件,沒有人會願意用它,它將變為一個極難使用的API。

確保你的文件無需授權便可被開發人員訪問。

不要使用自動文件生成器,如果用了,就一定要仔細檢查和修改生成的文件,確保它們能夠被使用者理解和使用。

文件中所舉例子中的請求和響應包體要寫全,不要截斷不重要的部分,而是用高亮方式展示重要的部分。

文件中要寫明各URL端點的預期返回碼和可能的出錯資訊,以及出現這些錯誤資訊的可能原因是什麼?

如果時間充足,你還可以構建一個第三方開發人員使用的API控制檯,以便他們可以立即對你的API進行驗證。這個事情做起來不會像你想象的那麼難,但是開發人員(包括內部開發人員和第三方開發人員)卻可能因為這個功能而喜歡上使用你的API。

確保文件能被列印,例如,CSS就是一種強大的文件展示方式;在文件列印的時候一定要隱藏文件的工具欄,即便沒有人把你的文件用印表機打印出來,也會有很多開發人員喜歡把它們輸出為PDF以便離線檢視。

HTTP包解析

我們所有的事情都是通過HTTP完成,經常令我感到吃驚的是現還有很多人並不知道HTTP包是什麼樣子,這裡我將給大家解析一個HTTP包。當用戶傳送一個HTTP請求到伺服器時,在請求包裡提供了一個key-value集合,叫做HTTP首部,緊隨其後的是兩個空行([逍遙子筆記:一個回車符一個換行符),最後是請求包體,所有這些都在同一個HTTP請求包內。

伺服器對請求的響應包中也是以Key-Value格式的首部,緊接著是兩個空行,然後是響應包體。HTTP是一種請求/響應協議,除非使用其他的協議,例如Websockets,否則服務端不會主動給客戶端推送訊息。

在設計API時,你最好藉助能夠檢視HTTP包體的工具來協助設計,例如:Wireshark。同時要確保所使用的框架/web伺服器能支援你儘可能多地讀取和修改這些包體的欄位資訊。

HTTP請求包示例

POST /v1/animal HTTP/1.1
Host: api.example.org
Accept: application/json
Content-Type: application/json
Content-Length: 24
 
{
  "name":"Gir",
  "animal_type": 12
}


HTTP響應包示例

HTTP/1.1 200 OK
Date: Wed, 18 Dec 2013 06:08:22 GMT
Content-Type: application/json
Access-Control-Max-Age: 1728000
Cache-Control: no-cache
 
{
  "id": 12,
  "created":1386363036,
  "modified":1386363036,
  "name":"Gir",
  "animal_type": 12
}