原文出處:http://www.csdn.net/article/2013-06-13/2815744-RESTful-API
背景
目前網際網路上充斥着大量的關于RESTful API(為了友善,以後API和RESTful API 一個意思)如何設計的文章,然而卻沒有一個”萬能“的設計标準:如何鑒權?API格式如何?你的API是否應該加入版本資訊?當你開始寫一個app的時候,特别是後端模型部分已經寫完的時候,你不得不殚精竭慮的設計和實作自己app的public API部分。因為一旦釋出,對外釋出的API将會很難改變。
在給SupportedFu設計API的時候,我試圖以實用的角度來解決上面提到的問題。我希望可以設計出容易使用,容易部署,并且足夠靈活的API,本文是以而生。
API設計的基本要求
網上的很多關于API設計的觀點都十分”學院派“,它們也許更有理論基礎,但是有時卻和現實世界脫軌(是以我是自由派)。是以我這篇文章的目标是從實踐的角度出發,給出目前網絡應用的API設計最佳實踐(當然,是我認為的最佳了~),如果覺得不合适,我不會遵從标準。當然作為設計的基礎,幾個必須的原則還是要遵守的:
- 當标準合理的時候遵守标準。
- API應該對程式員友好,并且在浏覽器位址欄容易輸入。
- API應該簡單,直覺,容易使用的同時優雅。
- API應該具有足夠的靈活性來支援上層ui。
- API設計權衡上述幾個原則。
需要強調的是:API的就是程式員的UI,和其他UI一樣,你必須仔細考慮它的使用者體驗!
使用RESTful URLs 和action.
雖然前面我說沒有一個萬能的API設計标準。但确實有一個被普遍承認和遵守:RESTfu設計原則。它被Roy Felding提出(在他的”基于網絡的軟體架構“論文中第五章)。而REST的核心原則是将你的API拆分為邏輯上的資源。這些資源通過http被操作(GET ,POST,PUT,DELETE)。
那麼我應該如何拆分出這些資源呢?
顯然從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的操作
對這個令人困惑的問題,下面是一些解決方法:
- 重構你的行為action。當你的行為不需要參數的時候,你可以把active對應到activated這個資源,(更新使用patch).
- 以子資源對待。例如:GitHub上,對一個gists加星操作:PUT /gists/:id/star 并且取消星操作:DELETE /gists/:id/star.
- 有時候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象下面這樣傳回結果:
[java]view plaincopy
- {
- "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,并且把真正的狀态碼放到傳回值裡面(包裝在信封裡),例如:
[js]view plaincopy
- 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:applicatin/json.否則抛出415異常(unsupported media type)。
分頁
分頁資料可以放到“信封”裡面,但随着标準的改進,現在我推薦将分頁資訊放到link header裡面:http://tools.ietf.org/html/rfc5988#page-6。
使用link header的API應該傳回一系列組合好了的url而不是讓使用者自己再去拼。這點在基于遊标的分頁中尤為重要。例如下面,來自github的文檔
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可以是一個逗号分隔的串,例如:
GET /ticket/12embed=customer.name,assigned_user
對應的API傳回值如下:
- "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格式的錯誤應該包含以下資訊:一個有用的錯誤資訊,一個唯一的錯誤碼,以及任何可能的詳細錯誤描述。如下:
- "code" : 1234,
- "message" : "Something bad happened <img src="http://blog.jobbole.com/wp-includes/p_w_picpaths/smilies/icon_sad.gif" alt=":-(" class="wp-smiley"> ",
- "description" : "More details about the error here"
對PUT,POST,PATCH的輸入的校驗也應該傳回相應的錯誤資訊,例如:
- "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"
- }
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 - 請求過多。