跟著 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 方法指定的。比較常用的方法有:
Verb | 描述 |
---|---|
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 設計中,經常用到的狀態碼以及它們的意義如下表:
狀態碼 | Label | 解釋 |
---|---|---|
200 | OK | 請求成功接收並處理,一般響應中都會有 body |
201 | Created | 請求已完成,並導致了一個或者多個資源被建立,最常用在 POST 建立資源的時候 |
202 | Accepted | 請求已經接收並開始處理,但是處理還沒有完成。一般用在非同步處理的情況,響應 body 中應該告訴客戶端去哪裡檢視任務的狀態 |
204 | No Content | 請求已經處理完成,但是沒有資訊要返回,經常用在 PUT 更新資源的時候(客戶端提供資源的所有屬性,因此不需要服務端返回)。如果有重要的 metadata,可以放到頭部返回 |
301 | Moved Permanently | 請求的資源已經永久性地移動到另外一個地方,後續所有的請求都應該直接訪問新地址。服務端會把新地址寫在 Location 頭部欄位,方便客戶端使用。允許客戶端把 POST 請求修改為 GET。 |
304 | Not Modified | 請求的資源和之前的版本一樣,沒有發生改變。用來快取資源,和條件性請求(conditional request)一起出現 |
307 | Temporary Redirect | 目標資源暫時性地移動到新的地址,客戶端需要去新地址進行操作,但是不能修改請求的方法。 |
308 | Permanent Redirect | 和 301 類似,除了客戶端不能修改原請求的方法 |
400 | Bad Request | 客戶端傳送的請求有錯誤(請求語法錯誤,body 資料格式有誤,body 缺少必須的欄位等),導致服務端無法處理 |
401 | Unauthorized | 請求的資源需要認證,客戶端沒有提供認證資訊或者認證資訊不正確 |
403 | Forbidden | 伺服器端接收到並理解客戶端的請求,但是客戶端的許可權不足。比如,普通使用者想操作只有管理員才有許可權的資源。 |
404 | Not Found | 客戶端要訪問的資源不存在,連結失效或者客戶端偽造 URL 的時候回遇到這個情況 |
405 | Method Not Allowed | 服務端接收到了請求,而且要訪問的資源也存在,但是不支援對應的方法。服務端必須返回 Allow 頭部,告訴客戶端哪些方法是允許的 |
415 | Unsupported Media Type | 服務端不支援客戶端請求的資源格式,一般是因為客戶端在 Content-Type 或者 Content-Encoding 中申明瞭希望的返回格式,但是服務端沒有實現。比如,客戶端希望收到 xml 返回,但是服務端支援 Json |
429 | Too Many Requests | 客戶端在規定的時間裡傳送了太多請求,在進行限流的時候會用到 |
500 | Internal Server Error | 伺服器內部錯誤,導致無法完成請求的內容 |
503 | Service Unavailable | 伺服器因為負載過高或者維護,暫時無法提供服務。伺服器端應該返回 Retry-After 頭部,告訴客戶端過一段時間再來重試 |
上面這些狀態碼覆蓋了 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,缺少必要的欄位,欄位的值不符合規定…… 而不是直接說“請求錯誤”之類的資訊。
10. 驗證和授權
一般來說,讓任何人隨意訪問公開的 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
- RESTful API 設計指南
- REST介面設計規範
- Restful API 首次被提出的論文:Architectural Styles and the Design of Network-based Software Architectures
相關文章
- GitHub 的 Restful HTTP API 設計分解GithubRESTHTTPAPI
- 跟著ChatGPT學習設計模式 - 工廠模式ChatGPT設計模式
- 跟著 Guava、Spring 學習如何設計觀察者模式GuavaSpring模式
- restful api設計指南RESTAPI
- 理解RESTful Api設計RESTAPI
- RESTful API 設計規範RESTAPI
- RESTFUL API 安全設計指南RESTAPI
- RestFul Api 設計 之 URLRESTAPI
- 跟著GPT學設計模式之代理模式GPT設計模式
- Vue-跟著李南江學程式設計Vue程式設計
- Restful API 的設計規範RESTAPI
- 跟著GPT學設計模式之橋接模式GPT設計模式橋接
- Laravel restful API 學習記錄一LaravelRESTAPI
- 跟著阿里學JavaDay02——Java程式設計起步阿里Java程式設計
- 好RESTful API的設計原則RESTAPI
- 綜合Twitter、Github等各大網站API設計經驗:RESTful API實用設計與最佳實踐 - Vinay SahniGithub網站APIREST
- Django RESTful API設計與實踐指南DjangoRESTAPI
- 跟著學習100天演算法演算法
- 大資料學習之路(跟著大神學習一波)大資料
- RESTful & “優雅的”API 響應結構設計RESTAPI
- 討論下 RESTful 風格 API 的路由設計RESTAPI路由
- 跟著Zepto學dom(二)
- 跟著菜鳥學pythonPython
- HTTP&RestFULHTTPREST
- 跟著 React 官方文件學 HooksReactHook
- 跟著廖雪峰學python 005Python
- GraphQL學習:Github GraphQL API v4初探GithubAPI
- 跟著GPT學習Java執行緒中斷機制GPTJava執行緒
- laravel5.6 RESTful API系列之整合github登入認證!LaravelRESTAPIGithub
- Go Web 程式設計--深入學習解析 HTTP 請求GoWeb程式設計HTTP
- 理解RESTful APIRESTAPI
- 【跟著大佬學JavaScript】之節流JavaScript
- 跟著超哥大神學習擴充套件包的製作- -套件
- JAVA程式設計學習記錄(API常用類(二))Java程式設計API
- 智慧|跟著美的集團學習VMI正確的開啟方式
- 跟著風變MTP管理課學習,職業發展不受限
- 【跟著我們學Golang】流程控制Golang
- 跟著Docker學分層複用思想Docker
- Swagger 文件工具 設計、構建、文件化和使用您的 RESTful APISwaggerRESTAPI