跟著 Github 學習 Restful HTTP API 設計

近幾年提供 HTTP API 服務的公司越來越多,許多公司都把 API 作為產品重要的一部分,作為服務提供出去。而微服務的興起,也讓企業內部開始重視和頻繁使用 HTTP API 。好的 HTTP API 設計容易理解、符合 RFC 標準、提供使用者便利的功能,其中經常被拿來作為教科書典範的當屬 Github API。這篇文章就通過 Github API 總結了一些非常好的設計原則,可以作為以後要編寫 HTTP API 的參考。

注意:這篇文章只討論設計原則,不是強制要求(API 設計者可以根據實際情況實現部分內容,甚至實現出和某些原則相反的內容),也不會給出實現的思路和細節。

1. 使用 HTTPS

這個和 Restful API 本身沒有很大的關係,但是對於增加網站的安全是非常重要的。特別如果你提供的是公開 API,用戶的信息洩露或者被攻擊會嚴重影響網站的信譽。

NOTE:不要讓非SSL的url訪問重定向到SSL的url。

2. API 地址和版本

在 url 中指定 API 的版本是個很好地做法。如果 API 變化比較大,可以把 API 設計為子域名,比如 https://api.github.com/v3;也可以簡單地把版本放在路徑中,比如 https://example.com/api/v1。

3. schema

對於響應返回的格式,JSON 因為它的可讀性、緊湊性以及多種語言支持等優點,成為了 HTTP API 最常用的返回格式。因此,最好採用 JSON 作為返回內容的格式。如果用戶需要其他格式,比如 xml,應該在請求頭部 Accept 中指定。對於不支持的格式,服務端需要返回正確的 status code,並給出詳細的說明。

4. 以資源為中心的 URL 設計

資源是 Restful API 的核心元素,所有的操作都是針對特定資源進行的。而資源就是 URL(Uniform Resoure Locator)表示的,所以簡潔、清晰、結構化的 URL 設計是至關重要的。Github 可以說是這方面的典範,下面我們就拿 repository 來說明。

/users/:username/repos
/users/:org/repos
/repos/:owner/:repo
/repos/:owner/:repo/tags
/repos/:owner/:repo/branches/:branch

我們可以看到幾個特性:

  • 資源分為單個文檔和集合,儘量使用複數來表示資源,單個資源通過添加 id 或者 name 等來表示
  • 一個資源可以有多個不同的 URL
  • 資源可以嵌套,通過類似目錄路徑的方式來表示,以體現它們之間的關係

NOTE: 根據RFC3986定義,URL是大小寫敏感的。所以為了避免歧義,儘量使用小寫字母。

5. 使用正確的 Method

有了資源的 URL 設計,所有針對資源的操作都是使用 HTTP 方法指定的。比較常用的方法有:

HEAD:只獲取某個資源的頭部信息。比如只想瞭解某個文件的大小,某個資源的修改日期等

GET:獲取資源

POST:創建資源

PATCH:更新資源的部分屬性。因為 PATCH 比較新,而且規範比較複雜,所以真正實現的比較少,一般都是用 POST 替代

PUT:替換資源,客戶端需要提供新建資源的所有屬性。如果新內容為空,要設置 Content-Length 為 0,以區別錯誤信息

DELETE:刪除資源

比如:

GET /repos/:owner/:repo/issues
GET /repos/:owner/:repo/issues/:number
POST /repos/:owner/:repo/issues
PATCH /repos/:owner/:repo/issues/:number
DELETE /repos/:owner/:repo

NOTE:更新和創建操作應該返回最新的資源,來通知用戶資源的情況;刪除資源一般不會返回內容。

不符合 CRUD 的情況

在實際資源操作中,總會有一些不符合 CRUD(Create-Read-Update-Delete) 的情況,一般有幾種處理方法。

使用 POST

為需要的動作增加一個 endpoint,使用 POST 來執行動作,比如 POST /resend 重新發送郵件。

增加控制參數

添加動作相關的參數,通過修改參數來控制動作。比如一個博客網站,會有把寫好的文章“發佈”的功能,可以用上面的 POST /articles/{:id}/publish 方法,也可以在文章中增加 published:boolean 字段,發佈的時候就是更新該字段 PUT /articles/{:id}?published=true

把動作轉換成資源

把動作轉換成可以執行 CRUD 操作的資源, github 就是用了這種方法。

比如“喜歡”一個 gist,就增加一個 /gists/:id/star 子資源,然後對其進行操作:“喜歡”使用 PUT /gists/:id/star,“取消喜歡”使用 DELETE /gists/:id/star。

另外一個例子是 Fork,這也是一個動作,但是在 gist 下面增加 forks資源,就能把動作變成 CRUD 兼容的:POST /gists/:id/forks 可以執行用戶 fork 的動作。

6. Query 讓查詢更自由

比如查詢某個 repo 下面 issues 的時候,可以通過以下參數來控制返回哪些結果:

  • state:issue 的狀態,可以是 open,closed,all
  • since:在指定時間點之後更新過的才會返回
  • assignee:被 assign 給某個 user 的 issues
  • sort:選擇排序的值,可以是 created、updated、comments
  • direction:排序的方向,升序(asc)還是降序(desc)
  • ……

7. 分頁 Pagination

當返回某個資源的列表時,如果要返回的數目特別多,比如 github 的 /users,就需要使用分頁分批次按照需要來返回特定數量的結果。

分頁的實現會用到上面提到的 url query,通過兩個參數來控制要返回的資源結果:

  • per_page:每頁返回多少資源,如果沒提供會使用預設的默認值;這個數量也是有一個最大值,不然用戶把它設置成一個非常大的值(比如 99999999)也失去了設計的初衷
  • page:要獲取哪一頁的資源,默認是第一頁

返回的資源列表為 [(page-1)*per_page, page*per_page)。github API 文檔中還提到一個很好的點,相關的分頁信息還可以存放到 Link 頭部,這樣客戶端可以直接得到諸如下一頁、最後一頁、上一頁等內容的 url 地址,而不是自己手動去計算和拼接。

8. 選擇合適的狀態碼

HTTP 應答中,需要帶一個很重要的字段:status code。它說明了請求的大致情況,是否正常完成、需要進一步處理、出現了什麼錯誤,對於客戶端非常重要。狀態碼都是三位的整數,大概分成了幾個區間:

  • 2XX:請求正常處理並返回
  • 3XX:重定向,請求的資源位置發生變化
  • 4XX:客戶端發送的請求有錯誤
  • 5XX:服務器端錯誤

在 HTTP API 設計中,經常用到的狀態碼以及它們的意義如下表:

跟著 Github 學習 Restful HTTP API 設計

上面這些狀態碼覆蓋了 API 設計中大部分的情況,如果對某個狀態碼不清楚或者希望查看更完整的列表,可以參考 HTTP Status Code 這個網站,或者 RFC7231 Response Status Codes 的內容。

9. 錯誤處理:給出詳細的信息

如果出錯的話,在 response body 中通過 message 給出明確的信息。

比如客戶端發送的請求有錯誤,一般會返回 4XX Bad Request 結果。這個結果很模糊,給出錯誤 message 的話,能更好地讓客戶端知道具體哪裡有問題,進行快速修改。

  • 如果請求的 JSON 數據無法解析,會返回 Problems parsing JSON
  • 如果缺少必要的 filed,會返回 422 Unprocessable Entity,除了 message 之外,還通過 errors 給出了哪些 field 缺少了,能夠方便調用方快速排錯

基本的思路就是儘可能提供更準確的錯誤信息:比如數據不是正確的 json,缺少必要的字段,字段的值不符合規定…… 而不是直接說“請求錯誤”之類的信息。

一般來說,讓任何人隨意訪問公開的 API 是不好的做法。驗證和授權是兩件事情:

  • 驗證(Authentication)是為了確定用戶是其申明的身份,比如提供賬戶的密碼。不然的話,任何人偽造成其他身份(比如其他用戶或者管理員)是非常危險的
  • 授權(Authorization)是為了保證用戶有對請求資源特定操作的權限。比如用戶的私人信息只能自己能訪問,其他人無法看到;有些特殊的操作只能管理員可以操作,其他用戶有隻讀的權限等等

如果沒有通過驗證(提供的用戶名和密碼不匹配,token 不正確等),需要返回 401 Unauthorized狀態碼,並在 body 中說明具體的錯誤信息;而沒有被授權訪問的資源操作,需要返回 403 Forbidden 狀態碼,還有詳細的錯誤信息。

NOTE:Github API 對某些用戶未被授權訪問的資源操作返回 404 Not Found,目的是為了防止私有資源的洩露(比如黑客可以自動化試探用戶的私有資源,返回 403 的話,就等於告訴黑客用戶有這些私有的資源)。

11. 限流 rate limit

如果對訪問的次數不加控制,很可能會造成 API 被濫用,甚至被 DDos 攻擊。根據使用者不同的身份對其進行限流,可以防止這些情況,減少服務器的壓力。

對用戶的請求限流之後,要有方法告訴用戶它的請求使用情況,Github API 使用的三個相關的頭部:

  • X-RateLimit-Limit: 用戶每個小時允許發送請求的最大值
  • X-RateLimit-Remaining:當前時間窗口剩下的可用請求數目
  • X-RateLimit-Rest: 時間窗口重置的時候,到這個時間點可用的請求數量就會變成 X-RateLimit-Limit 的值

如果允許沒有登錄的用戶使用 API(可以讓用戶試用),可以把 X-RateLimit-Limit 的值設置得很小,比如 Github 使用的 60。沒有登錄的用戶是按照請求的 IP 來確定的,而登錄的用戶按照認證後的信息來確定身份。

對於超過流量的請求,可以返回 429 Too many requests 狀態碼,並附帶錯誤信息。而 Github API 返回的是 403 Forbidden,雖然沒有 429 更準確,也是可以理解的。

Github 更進一步,提供了不影響當然 RateLimit 的請求查看當前 RateLimit 的接口

GET /rate_limit

12. Hypermedia API

Restful API 的設計最好做到 Hypermedia:在返回結果中提供相關資源的鏈接。這種設計也被稱為 HATEOAS。這樣做的好處是,用戶可以根據返回結果就能得到後續操作需要訪問的地址。

比如訪問 api.github.com,就可以看到 Github API 支持的資源操作。

13. 編寫優秀的文檔

API 最終是給人使用的,不管是公司內部,還是公開的 API 都是一樣。即使我們遵循了上面提到的所有規範,設計的 API 非常優雅,用戶還是不知道怎麼使用我們的 API。最後一步,但非常重要的一步是:為你的 API 編寫優秀的文檔。

對每個請求以及返回的參數給出說明,最好給出一個詳細而完整地示例,提醒用戶需要注意的地方……反正目標就是用戶可以根據你的文檔就能直接使用 API,而不是要發郵件給你,或者跑到你的座位上問你一堆問題。

  • Github API v3
  • https://developer.github.com/v3/
  • RESTful API 設計指南
  • http://www.ruanyifeng.com/blog/2014/05/restful_api.html
  • REST接口設計規範
  • http://wangwei.info/about-rest-api/
  • Restful API 首次被提出的論文:Architectural Styles and the Design of Network-based Software Architectures
  • http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm


分享到:


相關文章: