****RESTful API 設計最佳實踐(APP後端API設計參考典範)

http://blog.jobbole.com/41233/javascript

背景html

目前互聯網上充斥着大量的關於RESTful API(爲方便,下文中「RESTful API 」簡寫爲「API」)如何設計的文章,然而卻沒有一個」萬能「的設計標準:如何鑑權?API 格式如何?你的API是否應該加入版本信息?當你開始寫一個app的時候,特別是後端模型部分已經寫完的時候,你不得不殫精竭慮的設計和實現本身app的public API部分。由於一旦發佈,對外發布的API將會很難改變。java

在給SupportedFu設計API的時候,我試圖以實用的角度來解決上面提到的問題。我但願能夠設計出容易使用,容易部署,而且足夠靈活的API,本文所以而生。python

 

API設計的基本要求git

網上的不少關於API設計的觀點都十分」學院派「,它們也許更有理論基礎,可是有時卻和現實世界脫軌(所以我是自由派)。因此我這篇文章的目標是從實踐的角度出發,給出當前網絡應用的API設計最佳實踐(固然,是我認爲的最佳了~),若是以爲不合適,我不會聽從標準。固然做爲設計的基礎,幾個必須的原則仍是要遵照的:程序員

  1. 當標準合理的時候遵照標準。
  2. API應該對程序員友好,而且在瀏覽器地址欄容易輸入。
  3. API應該簡單,直觀,容易使用的同時優雅。
  4. API應該具備足夠的靈活性來支持上層ui。
  5. API設計權衡上述幾個原則。

須要強調的是:API的就是程序員的UI,和其餘UI同樣,你必須仔細考慮它的用戶體驗!github

 

使用RESTful URLs 和action.編程

雖然前面我說沒有一個萬能的API設計標準。但確實有一個被廣泛認可和遵照:RESTfu設計原則。它被Roy Felding提出(在他的」基於網絡的軟件架構「論文中第五章)。而REST的核心原則是將你的API拆分爲邏輯上的資源。這些資源經過http被操做(GET ,POST,PUT,DELETE)。json

 

那麼我應該如何拆分出這些資源呢?c#

顯然從API用戶的角度來看,」資源「應該是個名詞。即便你的內部數據模型和資源已經有了很好的對應,API設計的時候你仍然不須要把它們一對一的都暴露出來。這裏的關鍵是隱藏內部資源,暴露必需的外部資源。

在SupportFu裏,資源是 ticket、user、group。

一旦定義好了要暴露的資源,你能夠定義資源上容許的操做,以及這些操做和你的API的對應關係:

  • GET /tickets # 獲取ticket列表
  • GET /tickets/12 # 查看某個具體的ticket
  • POST /tickets # 新建一個ticket
  • PUT /tickets/12 # 更新ticket 12.
  • DELETE /tickets/12 #刪除ticekt 12

能夠看出使用REST的好處在於能夠充分利用http的強大實現對資源的CURD功能。而這裏你只須要一個endpoint:/tickets,再沒有其餘什麼命名規則和url規則了,cool!

 

這個endpoint的單數複數

一個能夠聽從的規則是:雖然看起來使用複數來描述某一個資源實例看起來彆扭,可是統一全部的endpoint,使用複數使得你的URL更加規整。這讓API使用者更加容易理解,對開發者來講也更容易實現。

如何處理關聯?關於如何處理資源之間的管理REST原則也有相關的描述:

  • GET /tickets/12/messages- Retrieves list of messages for ticket #12
  • GET /tickets/12/messages/5- Retrieves message #5 for ticket #12
  • POST /tickets/12/messages- Creates a new message in ticket #12
  • PUT /tickets/12/messages/5- Updates message #5 for ticket #12
  • PATCH /tickets/12/messages/5- Partially updates message #5 for ticket #12
  • DELETE /tickets/12/messages/5- Deletes message #5 for ticket #12

其中,若是這種關聯和資源獨立,那麼咱們能夠在資源的輸出表示中保存相應資源的endpoint。而後API的使用者就能夠經過點擊連接找到相關的資源。若是關聯和資源聯繫緊密。資源的輸出表示就應該直接保存相應資源信息。(例如這裏若是message資源是獨立存在的,那麼上面 GET /tickets/12/messages就會返回相應message的連接;相反的若是message不獨立存在,他和ticket依附存在,則上面的API調用返回直接返回message信息)

不符合CURD的操做

對這個使人困惑的問題,下面是一些解決方法:

  1. 重構你的行爲action。當你的行爲不須要參數的時候,你能夠把active對應到activated這個資源,(更新使用patch).
  2. 以子資源對待。例如:github上,對一個gists加星操做:PUT /gists/:id/star 而且取消星操做:DELETE /gists/:id/star.
  3. 有時候action實在沒有難以和某個資源對應上例如search。那就這麼辦吧。我認爲API的使用者對於/search這種url也不會有太大意見的(畢竟他很容易理解)。只要注意在文檔中寫清楚就能夠了。

永遠使用SSL

毫無例外,永遠都要使用SSL。你的應用不知道要被誰,以及什麼狀況訪問。有些是安全的,有些不是。使用SSL能夠減小鑑權的成本:你只須要一個簡單的令牌(token)就能夠鑑權了,而不是每次讓用戶對每次請求籤名。

值得注意的是:不要讓非SSL的url訪問重定向到SSL的url。

 

文檔

文檔和API自己同樣重要。文檔應該容易找到,而且公開(把它們藏到pdf裏面或者存到須要登陸的地方都不太好)。文檔應該有展現請求和輸出的例子:或者以點擊連接的方式或者經過curl的方式(請見openstack的文檔)。若是有更新(特別是公開的API),應該及時更新文檔。文檔中應該有關於什麼時候棄用某個API的時間表以及詳情。使用郵件列表或者博客記錄是好方法。

 

版本化

在API上加入版本信息能夠有效的防止用戶訪問已經更新了的API,同時也能讓不一樣主要版本之間平穩過渡。關因而否將版本信息放入url仍是放入請求頭有過爭論:API version should be included in the URL or in a header. 學術界說它應該放到header裏面去,可是若是放到url裏面咱們就能夠跨版本的訪問資源了。。(參考openstack)。

strip使用的方法就很好:它的url裏面有主版本信息,同時請求頭倆面有子版本信息。這樣在子版本變化過程當中url的穩定的。變化有時是不可避免的,關鍵是如何管理變化。完整的文檔和合理的時間表都會使得API使用者使用的更加輕鬆。

 

結果過濾,排序,搜索:

url最好越簡短越好,和結果過濾,排序,搜索相關的功能都應該經過參數實現(而且也很容易實現)。

過濾:爲全部提供過濾功能的接口提供統一的參數。例如:你想限制get /tickets 的返回結果:只返回那些open狀態的ticket–get /tickektsstate=open這裏的state就是過濾參數。

排序:和過濾同樣,一個好的排序參數應該可以描述排序規則,而不業務相關。複雜的排序規則應該經過組合實現:

  • GET /ticketssort=-priority- Retrieves a list of tickets in descending order of priority
  • GET /ticketssort=-priority,created_at- Retrieves a list of tickets in descending order of priority. Within a specific priority, older tickets are ordered first

這裏第二條查詢中,排序規則有多個rule以逗號間隔組合而成。

搜索:有些時候簡單的排序是不夠的。咱們可使用搜索技術(ElasticSearch和Lucene)來實現(依舊能夠做爲url的參數)。

  • GET /ticketsq=return&state=open&sort=-priority,created_at- Retrieve the highest priority open tickets mentioning the word ‘return’

對於常用的搜索查詢,咱們能夠爲他們設立別名,這樣會讓API更加優雅。例如:
get /ticketsq=recently_closed -> get /tickets/recently_closed.

 

限制API返回值的域

有時候API使用者不須要全部的結果,在進行橫向限制的時候(例如值返回API結果的前十項)還應該能夠進行縱向限制。而且這個功能能有效的提升網絡帶寬使用率和速度。可使用fields查詢參數來限制返回的域例如:
GET /ticketsfields=id,subject,customer_name,updated_at&state=open&sort=-updated_at

 

更新和建立操做應該返回資源

PUT、POST、PATCH 操做在對資源進行操做的時候經常有一些反作用:例如created_at,updated_at 時間戳。爲了防止用戶屢次的API調用(爲了進行這次的更新操做),咱們應該會返回更新的資源(updated representation.)例如:在POST操做之後,返回201 created 狀態碼,而且包含一個指向新資源的url做爲返回頭

 

是否須要 「HATEOAS

網上關因而否容許用戶建立新的url有很大的異議(注意不是建立資源產生的url)。爲此REST制定了HATEOAS來描述了和endpoint進行交互的時候,行爲應該在資源的metadata返回值裏面進行定義。

(譯註:做者這裏認爲HATEOAS還不算成熟,我也不怎麼理解這段就算了,讀者感興趣能夠本身去原文查看)

 

只提供json做爲返回格式

如今開始比較一下XML和json了。XML即冗長,難以閱讀,又不適合各類編程語言解析。固然XML有擴展性的優點,可是若是你只是將它來對內部資源串行化,那麼他的擴展優點也發揮不出來。不少應用(youtube,twitter,box)都已經開始拋棄XML了,我也不想多費口舌。給了google上的趨勢圖吧:

固然若是的你使用用戶裏面企業用戶居多,那麼可能須要支持XML。若是是這樣的話你還有另一個問題:你的http請求中的media類型是應該和accept 頭同步仍是和url?爲了方便(browser explorability),應該是在url中(用戶只要本身拼url就行了)。若是這樣的話最好的方法是使用.xml或者.json的後綴。

 

命名方式?

是蛇形命令(下劃線和小寫)仍是駝峯命名?若是使用json那麼最好的應該是遵照JAVASCRIPT的命名方法-也就是說駱駝命名法。若是你正在使用多種語言寫一個庫,那麼最好按照那些語言所推薦的,java,c#使用駱駝,python,ruby使用snake。

我的意見:我總以爲蛇形命令更好使一些,固然這沒有什麼理論的依據。有人說蛇形命名讀起來更快,能達到20%,也不知道真假http://ieeexplore.ieee.org/xpl/articleDetails.jsptp=&arnumber=5521745

 

默認使用pretty print格式,使用gzip

只是使用空格的返回結果從瀏覽器上看老是以爲很噁心(一大坨有沒有?~)。固然你能夠提供url上的參數來控制使用「pretty print」,可是默認開啓這個選項仍是更加友好。格外的傳輸上的損失不會太大。相反你若是忘了使用gzip那麼傳輸效率將會大大減小,損失大大增長。想象一個用戶正在debug那麼默認的輸出就是可讀的-而不用將結果拷貝到其餘什麼軟件中在格式化-是想起來就很爽的事,不是麼?

下面是一個例子:

$ curl https://API.github.com/users/veesahni > with-whitespace.txt
$ ruby -r json -e 'puts JSON JSON.parse(STDIN.read)' < with-whitespace.txt > without-whitespace.txt
$ gzip -c with-whitespace.txt > with-whitespace.txt.gz
$ gzip -c without-whitespace.txt > without-whitespace.txt.gz

輸出以下:

  • without-whitespace.txt- 1252 bytes
  • with-whitespace.txt- 1369 bytes
  • without-whitespace.txt.gz- 496 bytes
  • with-whitespace.txt.gz- 509 bytes

在上面的例子中,多餘的空格使得結果大小多出了8.5%(沒有使用gzip),相反只多出了2.6%。聽說:twitter使用gzip以後它的streaming API傳輸減小了80%(link:https://dev.twitter.com/blog/announcing-gzip-compression-streaming-APIs).

 

只在須要的時候使用「envelope」

不少API象下面這樣返回結果:

1
2
3
4
5
6
{
   "data" : {
     "id" : 123,
     "name" : "John"
   }
}

理由很簡單:這樣作能夠很容易擴展返回結果,你能夠加入一些分頁信息,一些數據的元信息等-這對於那些不容易訪問到返回頭的API使用者來講確實有用,可是隨着「標準」的發展(cors和http://tools.ietf.org/html/rfc5988#page-6都開始被加入到標準中了),我我的推薦不要那麼作。

什麼時候使用envelope?

有兩種狀況是應該使用envelope的。若是API使用者確實沒法訪問返回頭,或者API須要支持交叉域請求(經過jsonp)。
jsonp請求在請求的url中包含了一個callback函數參數。若是給出了這個參數,那麼API應該返回200,而且把真正的狀態碼放到返回值裏面(包裝在信封裏),例如:

1
2
3
4
5
6
7
callback_function({
   status_code: 200,
   next_page: "https://.." ,
   response: {
     ... actual JSON response body ...
   }
})

一樣爲了支持沒法方法返回頭的API使用者,能夠容許envelope=true這樣的參數。

在post,put,patch上使用json做爲輸入

若是你認同我上面說的,那麼你應該決定使用json做爲全部的API輸出格式,那麼咱們接下來考慮考慮API的輸入數據格式。
不少的API使用url編碼格式:就像是url查詢參數的格式同樣:單純的鍵值對。這種方法簡單有效,可是也有本身的問題:它沒有數據類型的概念。這使得程序不得不根據字符串解析出布爾和整數,並且尚未層次結構–雖然有一些關於層次結構信息的約定存在但是和自己就支持層次結構的json比較一下仍是不很好用。

固然若是API自己就很簡單,那麼使用url格式的輸入沒什麼問題。但對於複雜的API你應該使用json。或者乾脆統一使用json。
注意使用json傳輸的時候,要求請求頭裏面加入:Content-Type:application/json.,不然拋出415異常(unsupported media type)。

 

分頁

分頁數據能夠放到「信封」裏面,但隨着標準的改進,如今我推薦將分頁信息放到link header裏面:http://tools.ietf.org/html/rfc5988#page-6。

使用link header的API應該返回一系列組合好了的url而不是讓用戶本身再去拼。這點在基於遊標的分頁中尤其重要。例以下面,來自github的文檔

1
2
Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next",
<https://api.github.com/user/repos?page=50&per_page=100>; rel="last"

自動加載相關的資源

不少時候,自動加載相關資源很是有用,能夠很大的提升效率。可是這卻和RESTful的原則相背。爲了如此,咱們能夠在url中添加參數:embed(或者expend)。embed能夠是一個逗號分隔的串,例如:

1
GET /ticket/12embed=customer.name,assigned_user

對應的API返回值以下:

1
2
3
4
5
6
7
8
9
10
11
12
{
   "id" : 12,
   "subject" : "I have a question!" ,
   "summary" : "Hi, ...." ,
   "customer" : {
     "name" : "Bob"
   },
   assigned_user: {
    "id" : 42,
    "name" : "Jim" ,
   }
}

值得提醒的是,這個功能有時候會很複雜,而且可能致使N+1 SELECT 問題

 

重寫HTTP方法

有的客戶端只能發出簡單的GET 和POST請求。爲了照顧他們,咱們能夠重寫HTTP請求。這裏沒有什麼標準,可是一個廣泛的方式是接受X-HTTP-Method-Override請求頭。

 

速度限制

爲了不請求氾濫,給API設置速度限制很重要。爲此 RFC 6585 引入了HTTP狀態碼429(too many requests)。加入速度設置以後,應該提示用戶,至於如何提示標準上沒有說明,不過流行的方法是使用HTTP的返回頭。

下面是幾個必須的返回頭(依照twitter的命名規則):

  • X-Rate-Limit-Limit :當前時間段容許的併發請求數
  • X-Rate-Limit-Remaining:當前時間段保留的請求數。
  • X-Rate-Limit-Reset:當前時間段剩餘秒數

爲何使用當前時間段剩餘秒數而不是時間戳?

時間戳保存的信息不少,可是也包含了不少沒必要要的信息,用戶只須要知道還剩幾秒就能夠再發請求了這樣也避免了clock skew問題

有些API使用UNIX格式時間戳,我建議不要那麼幹。爲何?HTTP 已經規定了使用 RFC 1123 時間格式

 

鑑權 Authentication

restful API是無狀態的也就是說用戶請求的鑑權和cookie以及session無關,每一次請求都應該包含鑑權證實。

經過使用ssl咱們能夠不用每次都提供用戶名和密碼:咱們能夠給用戶返回一個隨機產生的token。這樣能夠極大的方便使用瀏覽器訪問API的用戶。這種方法適用於用戶能夠首先經過一次用戶名-密碼的驗證並獲得token,而且能夠拷貝返回的token到之後的請求中。若是不方便,可使用OAuth 2來進行token的安全傳輸。

支持jsonp的API須要額外的鑑權方法,由於jsonp請求沒法發送普通的credential。這種狀況下能夠在查詢url中添加參數:access_token。注意使用url參數的問題是:目前大部分的網絡服務器都會講query參數保存到服務器日誌中,這可能會成爲大的安全風險。

注意上面說到的只是三種傳輸token的方法,實際傳輸的token多是同樣的。

 

緩存

HTTP提供了自帶的緩存框架。你須要作的是在返回的時候加入一些返回頭信息,在接受輸入的時候加入輸入驗證。基本兩種方法:

ETag:當生成請求的時候,在HTTP頭裏面加入ETag,其中包含請求的校驗和和哈希值,這個值和在輸入變化的時候也應該變化。若是輸入的HTTP請求包含IF-NONE-MATCH頭以及一個ETag值,那麼API應該返回304 not modified狀態碼,而不是常規的輸出結果。

Last-Modified:和etag同樣,只是多了一個時間戳。返回頭裏的Last-Modified:包含了 RFC 1123 時間戳,它和IF-MODIFIED-SINCE一致。HTTP規範裏面有三種date格式,服務器應該都能處理。

出錯處理

就像html錯誤頁面可以顯示錯誤信息同樣,API 也應該能返回可讀的錯誤信息–它應該和通常的資源格式一致。API應該始終返回相應的狀態碼,以反映服務器或者請求的狀態。API的錯誤碼能夠分爲兩部分,400系列和500系列,400系列代表客戶端錯誤:如錯誤的請求格式等。500系列表示服務器錯誤。API應該至少將全部的400系列的錯誤以json形式返回。若是可能500系列的錯誤也應該如此。json格式的錯誤應該包含如下信息:一個有用的錯誤信息,一個惟一的錯誤碼,以及任何可能的詳細錯誤描述。以下:

1
2
3
4
5
{
   "code" : 1234,
   "message" : "Something bad happened :-(" ,
   "description" : "More details about the error here"
}

對PUT,POST,PATCH的輸入的校驗也應該返回相應的錯誤信息,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
   "code" : 1024,
   "message" : "Validation Failed" ,
   "errors" : [
     {
       "code" : 5432,
       "field" : "first_name" ,
       "message" : "First name cannot have fancy characters"
     },
     {
        "code" : 5622,
        "field" : "password" ,
        "message" : "Password cannot be blank"
     }
   ]
}

 

HTTP 狀態碼

1
2
3
4
5
6
7
8
9
10
11
12
200 ok  - 成功返回狀態,對應,GET,PUT,PATCH,DELETE.
  201 created  - 成功建立。
  304 not modified   - HTTP緩存有效。
  400 bad request   - 請求格式錯誤。
  401 unauthorized   - 未受權。
  403 forbidden   - 鑑權成功,可是該用戶沒有權限。
  404 not found - 請求的資源不存在
  405 method not allowed - 該http方法不被容許。
  410 gone - 這個url對應的資源如今不可用。
  415 unsupported media type - 請求類型錯誤。
  422 unprocessable entity - 校驗錯誤時用。
  429 too many request - 請求過多。
相關文章
相關標籤/搜索