怎樣設計一個好的 RESTful API
做出一個好的API設計很難。
API表達的是你的資料和你的資料使用者之間的契約。打破這個契約將會招致很多憤怒的郵件,和一大堆傷心的使用者-因為他們手機上的App不工作了。而文件化只能達到一半的效果,並且也很難找到一個願意寫文件的程式設計師。
你所能做的最重要一件事來提高服務的價值就是建立一個API。因為隨著其他服務的成長,有這樣一個API會使你的服務或者核心應用將有機會變成一個平臺。環顧一下現有的這些大公司:Facebook,Twitter,Google, Github,Amazon,Netflix等。如果當時他們沒有通過API來開放資料的話,也不可能成長到如今的規模。事實上,整個行業存在的唯一目的就是消費所謂平臺上的資料。
你的API越容易使用,那麼就會有越多的人去用它。
本文提到的這些原則,如果你的API能嚴格按照這些原則來設計,使用者就可以知道它接下來要做什麼,並且能減少大量不必要的疑惑或者是憤怒的郵件。我已經把所有內容都整理到不同的主題裡了,你無需按順序去閱讀它。
定義
這裡有一些非常重要的術語,我將在本文裡面一直用到它們:
資源:一個物件的單獨例項,如一隻動物
集合:一群同種物件,如動物
HTTP:跨網路的通訊協議
客戶端:可以建立HTTP請求的客戶端應用程式
第三方開發者:這個開發者不屬於你的專案但是有想使用你的資料
伺服器:一個HTTP伺服器或者應用程式,客戶端可以跨網路訪問它
端點:這個API在伺服器上的URL用於表達一個資源或者一個集合
冪等:無邊際效應,多次操作得到相同的結果
URL段:在URL裡面已斜槓分隔的內容
資料設計與抽象
規劃好你的API的外觀要先於開發它實際的功能。首先你要知道資料該如何設計和核心服務/應用程式會如何工作。如果你純粹新開發一個API,這樣會比較容易一些。但如果你是往已有的專案中增加API,你可能需要提供更多的抽象。
有時候一個集合可以表達一個數據庫表,而一個資源可以表達成裡面的一行記錄,但是這並不是常態。事實上,你的API應該儘可能通過抽象來分離資料與業務邏輯。這點非常重要,只有這樣做你才不會打擊到那些擁有複雜業務的第三方開發者,否則他們是不會使用你的API的。
當然你的服務可能很多部分是不應該通過API暴露出去的。比較常見的例子就是很多API是不允許第三方來建立使用者的。
動詞
顯然你瞭解GET和POST請求。當你用瀏覽器去訪問不同頁面的時候,這兩個是最常見的請求。POST術語如此流行以至於開始侵擾通俗用語。即使是那些不知道網際網路如何工作的人們也能“post”一些東西到朋友的Facebook牆上。
這裡至少有四個半非常重要的HTTP動詞需要你知道。我之所以說“半個”的意思是PATCH這個動詞非常類似於PUT,並且它們倆也常常被開發者繫結到同一個API上。
GET (選擇):從伺服器上獲取一個具體的資源或者一個資源列表。
POST (建立): 在伺服器上建立一個新的資源。
PUT (更新):以整體的方式更新伺服器上的一個資源。
PATCH (更新):只更新伺服器上一個資源的一個屬性。
DELETE (刪除):刪除伺服器上的一個資源。
還有兩個不常用的HTTP動詞:
HEAD : 獲取一個資源的元資料,如資料的雜湊值或最後的更新時間。
OPTIONS:獲取客戶端能對資源做什麼操作的資訊。
一個好的RESTful API只允許第三方呼叫者使用這四個半HTTP動詞進行資料互動,並且在URL段裡面不出現任何其他的動詞。
一般來說,GET請求可以被瀏覽器快取(通常也是這樣的)。例如,快取請求頭用於第二次使用者的POST請求。HEAD請求是基於一個無響應體的GET請求,並且也可以被快取的。
版本化
無論你正在構建什麼,無論你在入手前做了多少計劃,你核心的應用總會發生變化,資料關係也會變化,資源上的屬性也會被增加或刪除。只要你的專案還活著,並且有大量的使用者在用,這種情況總是會發生。
請謹記一點,API是伺服器與客戶端之間的一個公共契約。如果你對伺服器上的API做了一個更改,並且這些更改無法向後相容,那麼你就打破了這個契約,客戶端又會要求你重新支援它。為了避免這樣的事情,你既要確保應用程式逐步的演變,又要讓客戶端滿意。那麼你必須在引入新版本API的同時保持舊版本API仍然可用。
注:如果你只是簡單的增加一個新的特性到API上,如資源上的一個新屬性或者增加一個新的端點,你不需要增加API的版本。因為這些並不會造成向後相容性的問題,你只需要修改文件即可。
隨著時間的推移,你可能宣告不再支援某些舊版本的API。申明不支援一個特性並不意味著關閉或者破壞它。而是告訴客戶端舊版本的API將在某個特定的時間被刪除,並且建議他們使用新版本的API。
一個好的RESTful API會在URL中包含版本資訊。另一種比較常見的方案是在請求頭裡面保持版本資訊。但是跟很多不同的第三方開發者一起工作後,我可以很明確的告訴你,在請求頭裡麵包含版本資訊遠沒有放在URL裡面來的容易。
分析
所謂API分析就是持續跟蹤那些正為人使用的API的版本和端點資訊。而這可能就跟每次請求都往資料庫增加一個整數那樣簡單。有很多的原因顯示API跟蹤分析是一個好主意,例如,對那些使用最廣泛的API來說效率是最重要的。
第三方開發者通常會關注API的構建目的,其中最重要的一個目的是你決定什麼時候不再支援某個版本。你需要明確的告知開發者他們正在使用那些即將被移除的API特性。這是一個很好的方式在你準備刪除舊的API之前去提醒他們進行升級。
當然第三方開發者的通知流程可以以某種條件被自動觸發,例如每當一個過時的特性上發生10000次請求時就發郵件通知開發者。
API根URL
無論你信不信,API的根地址很重要。當一個開發者接手了一箇舊專案(如進行程式碼考古時)。而這個專案正在使用你的API,同時開發者還想構建一個新的特性,但他們完全不知道你的服務。幸運的是他們知道客戶端對外呼叫的那些URL列表。讓你的API根入口點保持儘可能的簡單是很重要的,因為開發者很可能一看到那些冗長而又複雜的URL就轉身而走。
這裡有兩個常見的URL根例子:
https://example.org/api/v1/*
https://api.example.com/v1/*
如果你的應用很龐大或者你預期它將會變的很龐大,那麼將API放到子域下通常是一個好選擇。這種做法可以保持某些規模化上的靈活性。
但如果你覺得你的API不會變的很龐大,或是你只是想讓應用安裝更簡單些(如你想用相同的框架來支援站點和API),將你的API放到根域名下也是可以的。
讓API根擁有一些內容通常也是個好主意。Github的API根就是一個典型的例子。從個人角度來說我是一個通過根URL釋出資訊的粉絲,這對很多人來說是有用的,例如如何獲取API相關的開發文件。
同樣也請注意HTTPS字首,一個好的RESTful API總是基於HTTPS來發布的。
端點
一個端點就是指向特定資源或資源集合的URL。
如果你正在構建一個虛構的API來展現幾個不同的動物園,每一個動物園又包含很多動物,員工和每個動物的物種,你可能會有如下的端點資訊:
https://api.example.com/v1/zoos
https://api.example.com/v1/animals
https://api.example.com/v1/animal_types
https://api.example.com/v1/employees
針對每一個端點來說,你可能想列出所有可行的HTTP動詞和端點的組合。如下所示,請注意我把HTTP動詞都放在了虛構的API之前,正如將同樣的註解放在每一個HTTP請求頭裡一樣。(下面的URL就不翻譯了,我覺得沒啥必要翻)
GET /zoos: List all Zoos (ID and Name, not too much detail)
POST /zoos: Create a new Zoo
GET /zoos/ZID: Retrieve an entire Zoo object
PUT /zoos/ZID: Update a Zoo (entire object)
PATCH /zoos/ZID: Update a Zoo (partial object)
DELETE /zoos/ZID: Delete a Zoo
GET /zoos/ZID/animals: Retrieve a listing of Animals (ID and Name).
GET /animals: List all Animals (ID and Name).
POST /animals: Create a new Animal
GET /animals/AID: Retrieve an Animal object
PUT /animals/AID: Update an Animal (entire object)
PATCH /animals/AID: Update an Animal (partial object)
GET /animal_types: Retrieve a listing (ID and Name) of all Animal Types
GET /animal_types/ATID: Retrieve an entire Animal Type object
GET /employees: Retrieve an entire list of Employees
GET /employees/EID: Retreive a specific Employee
GET /zoos/ZID/employees: Retrieve a listing of Employees (ID and Name) who work at this Zoo
POST /employees: Create a new Employee
POST /zoos/ZID/employees: Hire an Employee at a specific Zoo
DELETE /zoos/ZID/employees/EID: Fire an Employee from a specific Zoo
在上面的列表裡,ZID表示動物園的ID, AID表示動物的ID,EID表示僱員的ID,還有ATID表示物種的ID。讓文件裡所有的東西都有一個關鍵字是一個好主意。
為了簡潔起見,我已經省略了所有API共有的URL字首。作為溝通方式這沒什麼問題,但是如果你真要寫到API文件中,那就必須包含完整的路徑(如,GET http://api.example.com/v1/animal_type/ATID)。
請注意如何展示資料之間的關係,特別是僱員與動物園之間的多對多關係。通過新增一個額外的URL段就可以實現更多的互動能力。當然沒有一個HTTP動詞能表示正在解僱一個人,但是你可以使用DELETE一個動物園裡的僱員來達到相同的效果。
過濾器
當客戶端建立了一個請求來獲取一個物件列表時,很重要一點就是你要返回給他們一個符合查詢條件的所有物件的列表。這個列表可能會很大。但你不能隨意給返回資料的數量做限制。因為這些無謂的限制會導致第三方開發者不知道發生了什麼。如果他們請求一個確切的集合並且要遍歷結果,然而他們發現只拿到了100條資料。接下來他們就不得不去查詢這個限制條件的出處。到底是ORM的bug導致的,還是因為網路截斷了大資料包?
儘可能減少那些會影響到第三方開發者的無謂限制。
這點很重要,但你可以讓客戶端自己對結果做一些具體的過濾或限制。這麼做最重要的一個原因是可以最小化網路傳輸,並讓客戶端儘可能快的得到查詢結果。其次是客戶端可能比較懶,如果這時伺服器能對結果做一些過濾或分頁,對大家都是好事。另外一個不那麼重要的原因是(從客戶端角度來說),對伺服器來說響應請求的負載越少越好。
過濾器是最有效的方式去處理那些獲取資源集合的請求。所以只要出現GET的請求,就應該通過URL來過濾資訊。以下有一些過濾器的例子,可能是你想要填加到API中的:
?limit=10: 減少返回給客戶端的結果數量(用於分頁)
?offset=10: 傳送一堆資訊給客戶端(用於分頁)
?animaltypeid=1: 使用條件匹配來過濾記錄
?sortby=name&order=asc: 對結果按特定屬性進行排序
有些過濾器可能會與端點URL的效果重複。例如我之前提到的GET /zoo/ZID/animals。它也同樣可以通過GET /animals?zoo_id=ZID來實現。獨立的端點會讓客戶端更好過一些,因為他們的需求往往超出你的預期。本文中提到這種冗餘差異可能對第三方開發者並不可見。
無論怎麼說,當你準備過濾或排序資料時,你必須明確的將那些客戶端可以過濾或排序的列放到白名單中,因為我們不想將任何的資料庫錯誤傳送給客戶端。
狀態碼
對於一個RESTful API來說很重要的一點就是要使用HTTP的狀態碼,因為它們是HTTP的標準。很多的網路裝置都可以識別這些狀態碼,例如負載均衡器可能會通過配置來避免傳送請求到一臺web伺服器,如果這臺伺服器已經發送了很多的50x錯誤回來。這裡有大量的HTTP狀態碼可以選擇,但是下面的列表只給出了一些重要的程式碼作為一個參考:
200 OK – [GET] 客戶端向伺服器請求資料,伺服器成功找到它們
201 CREATED – [POST/PUT/PATCH] 客戶端向伺服器提供資料,伺服器根據要求建立了一個資源
204 NO CONTENT – [DELETE] 客戶端要求伺服器刪除一個資源,伺服器刪除成功
400 INVALID REQUEST – [POST/PUT/PATCH] 客戶端向伺服器提供了不正確的資料,伺服器什麼也沒做
404 NOT FOUND – [] 客戶端引用了一個不存在的資源或集合,伺服器什麼也沒做
500 INTERNAL SERVER ERROR – [] 伺服器發生內部錯誤,客戶端無法得知結果,即便請求已經處理成功
狀態碼範圍
1xx範圍的狀態碼是保留給底層HTTP功能使用的,並且估計在你的職業生涯裡面也用不著手動傳送這樣一個狀態碼出來。
2xx範圍的狀態碼是保留給成功訊息使用的,你儘可能的確保伺服器總髮送這些狀態碼給使用者。
3xx範圍的狀態碼是保留給重定向用的。大多數的API不會太常使用這類狀態碼,但是在新的超媒體樣式的API中會使用更多一些。
4xx範圍的狀態碼是保留給客戶端錯誤用的。例如,客戶端提供了一些錯誤的資料或請求了不存在的內容。這些請求應該是冪等的,不會改變任何伺服器的狀態。
5xx範圍的狀態碼是保留給伺服器端錯誤用的。這些錯誤常常是從底層的函式丟擲來的,並且開發人員也通常沒法處理。傳送這類狀態碼的目的是確保客戶端能得到一些響應。收到5xx響應後,客戶端沒辦法知道伺服器端的狀態,所以這類狀態碼是要儘可能的避免。
預期的返回文件
當使用不同的HTTP動詞向伺服器請求時,客戶端需要在返回結果裡面拿到一系列的資訊。下面的列表是非常經典的RESTful API定義:
GET /collection: 返回一系列資源物件
GET /collection/resource: 返回單獨的資源物件
POST /collection: 返回新建立的資源物件
PUT /collection/resource: 返回完整的資源物件
PATCH /collection/resource: 返回完整的資源物件
DELETE /collection/resource: 返回一個空文件
請注意當一個客戶端建立一個資源時,她們常常不知道新建資源的ID(也許還有其他的屬性,如建立和修改的時間戳等)。這些屬性將在隨後的請求中返回,並且作為剛才POST請求的一個響應結果。
認證
伺服器在大多數情況下是想確切的知道誰建立了什麼請求。當然,有些API是提供給公共使用者(匿名使用者)的,但是大部分時間裡也是代表某人的利益。
OAuth2.0提供了一個非常好的方法去做這件事。在每一個請求裡,你可以明確知道哪個客戶端建立了請求,哪個使用者提交了請求,並且提供了一種標準的訪問過期機制或允許使用者從客戶端登出,所有這些都不需要第三方的客戶端知道使用者的登陸認證資訊。
還有OAuth1.0和xAuth同樣適用這樣的場景。無論你選擇哪個方法,請確保它為多種不同語言/平臺上的庫提供了一些通用的並且設計良好文件,因為你的使用者可能會使用這些語言和平臺來編寫客戶端。
內容型別
目前,大多數“精彩”的API都為RESTful介面提供JSON資料。諸如Facebook,Twitter,Github等等你所知的。XML曾經也火過一把(通常在一個大企業級環境下)。這要感謝SOAP,不過它已經掛了,並且我們也沒看到太多的API把HTML作為結果返回給客戶端(除非你在構建一個爬蟲程式)。
只要你返回給他們有效的資料格式,開發者就可以使用流行的語言和框架進行解析。如果你正在構建一個通用的響應物件,通過使用一個不同的序列化器,你也可以很容易的提供之前所提到的那些資料格式(不包括SOAP)。而你所要做的就是把使用方式放在響應資料的接收頭裡面。
有些API的建立者會推薦把.json, .xml, .html等檔案的副檔名放在URL裡面來指示返回內容型別,但我個人並不習慣這麼做。我依然喜歡通過接收頭來指示返回內容型別(這也是HTTP標準的一部分),並且我覺得這麼做也比較適當一些。
超媒體API
超媒體API很可能就是RESTful API設計的將來。超媒體是一個非常棒的概念,它迴歸到了HTTP和HTML如何運作的“本質”。
在非超媒體RESTful API的情景中,URL端點是伺服器與客戶端契約的一部分。這些端點必須讓客戶端事先知道,並且修改它們也意味著客戶端可能再也無法與伺服器通訊了。你可以先假定這是一個限制。
時至今日,英特網上的API客戶端已經不僅僅只有那些建立HTTP請求的使用者代理了。大多數HTTP請求是由人們通過瀏覽器產生的。人們不會被哪些預先定義好的RESTful API端點URL所束縛。是什麼讓人們變的如此與眾不同?因為人們可以閱讀內容,可以點選他們感興趣的連結,並瀏覽一下網站,然後跳到他們關注的內容那裡。即使一個URL改變了,人們也不會受到影響(除非他們事先給某個頁面做了書籤,這時他們回到主頁並發現原來有一條新的路徑可以去往之前的頁面)。
超媒體API概念的運作跟人們的行為類似。通過請求API的根來獲得一個URL的列表,這個列表裡面的每一個URL都指向一個集合,並且提供了客戶端可以理解的資訊來描述每一個集合。是否為每一個資源提供ID並不重要(或者不是必須的),只要提供URL即可。
一個超媒體API一旦具有了客戶端,那麼它就可以爬行連結並收集資訊,而URL總是在響應中被更新,並且不需要如契約的一部分那樣事先被知曉。如果一個URL曾經被快取過,並且在隨後的請求中返回404錯誤,那麼客戶端可以很簡單的回退到根URL並重新發現內容。
在獲取集合中的一個資源列表時會返回一個屬性,這個屬性包含了各個資源的完整URL。當實施一個POST/PATCH/PUT請求後,響應可以被一個3xx的狀態碼重定向到完整的資源上。
JSON不僅告訴了我們需要定義哪些屬性作為URL,也告訴了我們如何將URL與當前文件關聯的語義。正如你猜的那樣,HTML就提供了這樣的資訊。我們可能很樂意看到我們的API走完了完整的週期,並回到了處理HTML上來。想一下我們與CSS一起前行了多遠,有一天我們可能再次看到它變成了一個通用實踐讓API和網站可以去使用相同的URL和內容。
文件
老實說,即使你不能百分之百的遵循指南中的條款,你的API也不是那麼糟糕。但是,如果你不為API準備文件的話,沒有人會知道怎麼使用它,那它真的會成為一個糟糕的API。
讓你的文件對那些未經認證的開發者也可用
不要使用文件自動化生成器,即便你用了,你也要保證自己審閱過並讓它具有更好的版式。
不要截斷示例中請求與響應的內容,要展示完整的東西。並在文件中使用高亮語法。
文件化每一個端點所預期的響應程式碼和可能的錯誤訊息,和在什麼情況下會產生這些的錯誤訊息
如果你有富餘的時間,那就建立一個控制檯來讓開發者可以立即體驗一下API的功能。建立一個控制檯並沒有想象中那麼難,並且開發者們(內部或者第三方)也會因此而擁戴你。
另外確保你的文件能夠被列印。CSS是個強大的工具可以幫助到你。而且在列印的時候也不用太擔心邊側欄的問題。即便沒有人會列印到紙上,你也會驚奇的發現很多開發者願意轉化成PDF格式進行離線閱讀。
勘誤:原始的HTTP封包
因為我們所做的都是基於HTTP協議,所以我將展示給你一個解析了的HTTP封包。我經常很驚訝的發現有多少人不知道這些東西。當客戶端傳送一個請求道伺服器時,他們會提供一個鍵值對集,先是一個頭,緊跟著是兩個回車換行符,然後才是請求體。所有這些都是在一個封包裡被髮送。
伺服器響應也是同樣的鍵值對集,帶兩個回車換行符,然後是響應體。HTTP就是一個請求/響應協議;它不支援“推送”模式(伺服器直接傳送資料給客戶端),除非你採用其他協議,如Websockets。
當你設計API時,你應該能夠使用工具去檢視原始的HTTP封包。Wireshark是個不錯的選擇。同時,你也該採用一個框架/web伺服器,使你能夠在必要時修改某些欄位的值。
Example HTTP Request
一個好的 RESTful API 需要注意這些……
Example HTTP Response
一個好的 RESTful API 需要注意這些……