近一年半,我參與了兩到三個項目的工做,這些項目涉及到大量供「外部」使用的REST API,稍後咱們會看到爲何要將「外部」這個詞放在引號之中。在項目工做期間,我不得不對這些API進行反覆地設計,再設計和重構,這篇文章是我對Rest API最佳實踐的一些我的見解,但願讀者可以從中獲益。javascript
對於不少語言來講,實現REST Service是一項極其微不足道的任務。換言之,不管你選擇什麼底層框架,只要輔以少許配置和代碼,你能夠在一小時以內就擁有一個REST Service。雖然對於缺少經驗的人來講,這確實很方便,但它也很容易讓你迅速寫出一個質量低下的API。所以,在你編寫代碼以前,先留出一分鐘的時間思考一下,試着去設計你的API,花足夠的時間去理解業務範疇,判斷客戶端須要從你的系統中獲取什麼。舉個例子,若是你的系統是針對一羣硬幣收藏家所創建的數據庫,此時你須要決定的是:你是否容許客戶端添加新的硬幣,或者僅僅容許取出原有的硬幣;客戶須要什麼樣的查詢方式;若是趕上涉及大量數據檢索的請求,你如何處理它?儘早地回答這些問題可以幫助你開發出更貼近用戶需求的API。java
如今已經頗有多關於資源(Resource)命名和組織的討論了,在這裏我基於本身的經驗再老調重彈一下,如下是三種易於遵循的規範。數據庫
1. 只使用名詞:舉個例子,若是你想提供一項在數據庫中搜索硬幣的服務,要避免將端點(Endpoint)命名爲/searchCoins或/findCoins或/getAllCoins 等等,一個簡單的/coins就已經足夠了,當客戶端發送一個GET請求的時候,能夠得到全部有效硬幣的集合。相似的,若是你想提供一項在數據庫中添加硬幣的服務,要避免使用諸如/addCoin或/saveCoin或/insertCointToDatabase這樣的名稱,你可使用與上面相同的資源名稱,要改變的僅僅是用POST請求代替GET請求。一樣地,對於更新硬幣,可使用PUT請求。json
2. 若是須要獲取單個硬幣,又應該怎麼作呢?我所建議的最佳方式是在端點中加入一個參數,好比說客戶端須要拿到一個ID是20的硬幣,那麼發送一個請求到/coins/20就足夠了。咱們再來看一個更復雜的例子,若是要讓客戶端可以爲每一個硬幣添加一張圖片,一個快速而醜陋的方式是/addCoinImage或/addNewImageToCoin等等,一個稍好一點的方式是/coins/addImage,可是正如我以前所說的,不該該有任何動詞存在。還記得咱們以前提到的獲取某種硬幣的方法嗎?咱們能夠將其稍微加強一下,發送POST請求給/coins/20/images如何?目前看起來很不錯。不過天下沒有完美的事物,假設一下,若是咱們要讓一些超級用戶可以從系統中刪除硬幣,根據咱們以前的討論,一個簡單的DELETE請求發送給/coins/{id}就足夠了,可是請你想一下,若是{id}僅僅是COINS表中的一個順序編號,那會產生多大的問題?某人能夠輕易地一個接一個的發送DELETE請求,最後系統中全部的數據全沒了。我想說的重點是,使用標識符做爲請求參數是不錯,可是前提是這些標識符必須很難猜想或根本沒法猜想。因此,若是你想要用一串序號去肯定一個實體,那就忘了這種實現吧。個人建議是,不要使用資源參數,直接發送一個DELETE請求給/coins,結合一個request body(好比json),其中含有足夠的參數可以定位所要被刪除的實體便可。安全
3. 儘量使用特定領域的名稱。若是你的業務域中有一羣硬幣收藏家(Coin Collectors),那麼當你設計API的時候,應當使用collectors這個詞,而不是users或accounts。要避免使用一些意義過於寬泛的名稱,這些名稱不能表示什麼,到了客戶端又容易產生誤解。對於請求參數的命名,道理也是同樣的。另外,強烈建議給請求參數取一個儘量短,同時又有意義的名稱,舉個例子,若是你想要查找在某一指定年份發行的硬幣,一個很讚的參數名稱是issueYear,比較典型的反例是:year(意義不明確),yearOfFirstIssue(包含無用信息)。服務器
對於這個話題,個人經驗是讓客戶端在每次發送請求後,不管結果是成功仍是失敗,都能得到相同格式的json響應,這將會給客戶端處理帶來極大的幫助。舉個例子,你想要添加一個新的硬幣,向/coins發送POST請求,一個成功的響應包含如下json文檔:框架
1
2
3
4
5
6
7
8
|
{
"meta"
:{
"code"
:200
},
"data"
:{
"coinId"
:
"a7sad-123kk-223"
}
}
|
一個錯誤的響應多是這樣的:工具
1
2
3
4
5
6
7
8
9
|
{
"meta"
:{
"code"
:60001,
"error"
:
"Can not add coin"
,
"info"
:
"Missing one ore more required fields"
},
"data"
:{
}
}
|
請注意,對全部可能的結果(成功或失敗),json響應的文檔都具有相同的結構,其中有兩種基本元素:meta和data,meta包含結果信息,在出錯的狀況下,其中還會包含一個特殊的錯誤碼(error code),在錯誤碼以後,」error」表示出錯的內容,」info」表示出錯的具體描述;data是可選的,包含從服務器返回的全部數據,就拿上面的例子來講,當添加硬幣成功後,服務器會返回一個惟一的自動生成的標識符,若是有錯誤,這項就爲空。這種作法的優點是,對於同一個API的各類服務類型和結果,客戶端均可以採用相同的方式進行處理。此外,當有意外狀況發生時,咱們也能夠傳遞一些額外的信息,正如上面例子中所展現的,」error」傳達信息,」info」記錄日誌。咱們還有一種選擇,能夠基於錯誤碼去處理響應,只要明確每一個數字的含義便可,請注意這些數字並不是http狀態碼,你依然要爲每一個請求返回正確的http狀態碼(如400、401等)。ui
在咱們討論下一節以前,我想強調另外一件值得重視的事,假設咱們不容許刪除硬幣,可是客戶端嘗試向/coins/{id}發送一個DELETE請求,一般狀況下Web容器會返回一個405的狀態碼,但我發現,若是咱們對這些響應進行過濾並返回相同的json文檔,會頗有幫助。好比咱們能夠返回:spa
1
2
3
4
5
6
7
8
9
|
{
"meta"
:{
"code"
:405,
"error"
:
"Method not allowed for the /coins/{id} resource"
,
"info"
:
"Method DELETE is not allowed for that resource. Available methods : GET, POST, OPTIONS"
},
"data"
:{
}
}
|
這比原來好多了,不是嗎?如今,響應內容不但包含原有的信息(405狀態碼),還通知客戶端該資源可用的方法。
最後但也是最重要的一點,花一點時間,提供一份專業的、對開發人員友好的文檔,並保證及時更新,一份過時文檔的危害性比沒有文檔更甚。你可使用一些開源免費的工具對你的API進行文檔化。再好一點的作法是,對每一項資源的使用方式都能提供範例,對成功或錯誤的響應都能提供預期結果。不要忘了,在最後要記錄下每個錯誤碼並提供完整的信息,這樣客戶端才能在錯誤發生時作出反應,有一些客戶端不會理會你的響應內容,它們會根據你的錯誤碼自行提供信息。
我還有若干個更爲實用的建議待寫,特別是關於API的版本控制和安全性方面的建議,但我想它們更適合在另外一篇博文中進行探討。
http://blog.jobbole.com/70511/