跟着 Github 学习 Restful HTTP API 的优雅设计
近幾年提供?HTTP API?服務(wù)的公司越來越多,許多公司都把 API 作為產(chǎn)品重要的一部分,作為服務(wù)提供出去。而微服務(wù)的興起,也讓企業(yè)內(nèi)部開始重視和頻繁使用 HTTP API 。好的?HTTP API設(shè)計容易理解、符合 RFC 標(biāo)準(zhǔn)、提供使用者便利的功能,其中經(jīng)常被拿來作為教科書典范的當(dāng)屬?Github API。這篇文章就通過?Github API?總結(jié)了一些非常好的設(shè)計原則,可以作為以后要編寫 HTTP API 的參考。
注意:這篇文章只討論設(shè)計原則,不是強制要求(API 設(shè)計者可以根據(jù)實際情況實現(xiàn)部分內(nèi)容,甚至實現(xiàn)出和某些原則相反的內(nèi)容),也不會給出實現(xiàn)的思路和細(xì)節(jié)。
1. 使用 HTTPS
這個和 Restful API 本身沒有很大的關(guān)系,但是對于增加網(wǎng)站的安全是非常重要的。特別如果你提供的是公開 API,用戶的信息泄露或者被攻擊會嚴(yán)重影響網(wǎng)站的信譽。
NOTE:不要讓非SSL的url訪問重定向到SSL的url。
2. API 地址和版本
在?url?中指定 API 的版本是個很好地做法。如果 API 變化比較大,可以把 API 設(shè)計為子域名,比如?https://api.github.com/v3;也可以簡單地把版本放在路徑中,比如?https://example.com/api/v1。
3. schema
對于響應(yīng)返回的格式,JSON 因為它的可讀性、緊湊性以及多種語言支持等優(yōu)點,成為了 HTTP API 最常用的返回格式。因此,最好采用 JSON 作為返回內(nèi)容的格式。如果用戶需要其他格式,比如?xml,應(yīng)該在請求頭部?Accept?中指定。對于不支持的格式,服務(wù)端需要返回正確的 status code,并給出詳細(xì)的說明。
4. 以資源為中心的 URL 設(shè)計
資源是?Restful API?的核心元素,所有的操作都是針對特定資源進(jìn)行的。而資源就是?URL(Uniform Resoure Locator)表示的,所以簡潔、清晰、結(jié)構(gòu)化的 URL 設(shè)計是至關(guān)重要的。Github 可以說是這方面的典范,下面我們就拿?repository?來說明。
/users/:username/repos /users/:org/repos /repos/:owner/:repo /repos/:owner/:repo/tags /repos/:owner/:repo/branches/:branch我們可以看到幾個特性:
-
資源分為單個文檔和集合,盡量使用復(fù)數(shù)來表示資源,單個資源通過添加 id 或者 name 等來表示
-
一個資源可以有多個不同的 URL
-
資源可以嵌套,通過類似目錄路徑的方式來表示,以體現(xiàn)它們之間的關(guān)系
NOTE: 根據(jù)RFC3986定義,URL是大小寫敏感的。所以為了避免歧義,盡量使用小寫字母。
5. 使用正確的 Method
有了資源的 URL 設(shè)計,所有針對資源的操作都是使用 HTTP 方法指定的。比較常用的方法有:
| HEAD | 只獲取某個資源的頭部信息。比如只想了解某個文件的大小,某個資源的修改日期等 |
| GET | 獲取資源 |
| POST | 創(chuàng)建資源 |
| PATCH | 更新資源的部分屬性。因為 PATCH 比較新,而且規(guī)范比較復(fù)雜,所以真正實現(xiàn)的比較少,一般都是用 POST 替代 |
| PUT | 替換資源,客戶端需要提供新建資源的所有屬性。如果新內(nèi)容為空,要設(shè)置?Content-Length為 0,以區(qū)別錯誤信息 |
| 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/:repoNOTE:更新和創(chuàng)建操作應(yīng)該返回最新的資源,來通知用戶資源的情況;刪除資源一般不會返回內(nèi)容。
不符合 CRUD 的情況
在實際資源操作中,總會有一些不符合?CRUD(Create-Read-Update-Delete) 的情況,一般有幾種處理方法。
使用 POST
為需要的動作增加一個 endpoint,使用 POST 來執(zhí)行動作,比如?POST /resend?重新發(fā)送郵件。
增加控制參數(shù)
添加動作相關(guān)的參數(shù),通過修改參數(shù)來控制動作。比如一個博客網(wǎng)站,會有把寫好的文章“發(fā)布”的功能,可以用上面的?POST /articles/{:id}/publish?方法,也可以在文章中增加?published:boolean?字段,發(fā)布的時候就是更新該字段?PUT /articles/{:id}?published=true
把動作轉(zhuǎn)換成資源
把動作轉(zhuǎn)換成可以執(zhí)行?CRUD?操作的資源, github 就是用了這種方法。
比如“喜歡”一個 gist,就增加一個?/gists/:id/star?子資源,然后對其進(jìn)行操作:“喜歡”使用?PUT /gists/:id/star,“取消喜歡”使用?DELETE /gists/:id/star。
另外一個例子是?Fork,這也是一個動作,但是在 gist 下面增加?forks資源,就能把動作變成?CRUD?兼容的:POST /gists/:id/forks?可以執(zhí)行用戶 fork 的動作。
6. Query 讓查詢更自由
比如查詢某個 repo 下面 issues 的時候,可以通過以下參數(shù)來控制返回哪些結(jié)果:
-
state:issue 的狀態(tài),可以是?open,closed,all
-
since:在指定時間點之后更新過的才會返回
-
assignee:被 assign 給某個 user 的 issues
-
sort:選擇排序的值,可以是?created、updated、comments
-
direction:排序的方向,升序(asc)還是降序(desc)
-
……
7. 分頁 Pagination
當(dāng)返回某個資源的列表時,如果要返回的數(shù)目特別多,比如 github 的?/users,就需要使用分頁分批次按照需要來返回特定數(shù)量的結(jié)果。
分頁的實現(xiàn)會用到上面提到的 url query,通過兩個參數(shù)來控制要返回的資源結(jié)果:
-
per_page:每頁返回多少資源,如果沒提供會使用預(yù)設(shè)的默認(rèn)值;這個數(shù)量也是有一個最大值,不然用戶把它設(shè)置成一個非常大的值(比如?99999999)也失去了設(shè)計的初衷
-
page:要獲取哪一頁的資源,默認(rèn)是第一頁
返回的資源列表為?[(page-1)*per_page, page*per_page)。github API 文檔中還提到一個很好的點,相關(guān)的分頁信息還可以存放到?Link?頭部,這樣客戶端可以直接得到諸如下一頁、最后一頁、上一頁等內(nèi)容的 url 地址,而不是自己手動去計算和拼接。
8. 選擇合適的狀態(tài)碼
HTTP 應(yīng)答中,需要帶一個很重要的字段:status code。它說明了請求的大致情況,是否正常完成、需要進(jìn)一步處理、出現(xiàn)了什么錯誤,對于客戶端非常重要。狀態(tài)碼都是三位的整數(shù),大概分成了幾個區(qū)間:
-
2XX:請求正常處理并返回
-
3XX:重定向,請求的資源位置發(fā)生變化
-
4XX:客戶端發(fā)送的請求有錯誤
-
5XX:服務(wù)器端錯誤
在 HTTP API 設(shè)計中,經(jīng)常用到的狀態(tài)碼以及它們的意義如下表:
| 200 | OK | 請求成功接收并處理,一般響應(yīng)中都會有 body |
| 201 | Created | 請求已完成,并導(dǎo)致了一個或者多個資源被創(chuàng)建,最常用在 POST 創(chuàng)建資源的時候 |
| 202 | Accepted | 請求已經(jīng)接收并開始處理,但是處理還沒有完成。一般用在異步處理的情況,響應(yīng) body 中應(yīng)該告訴客戶端去哪里查看任務(wù)的狀態(tài) |
| 204 | No Content | 請求已經(jīng)處理完成,但是沒有信息要返回,經(jīng)常用在 PUT 更新資源的時候(客戶端提供資源的所有屬性,因此不需要服務(wù)端返回)。如果有重要的 metadata,可以放到頭部返回 |
| 301 | Moved Permanently | 請求的資源已經(jīng)永久性地移動到另外一個地方,后續(xù)所有的請求都應(yīng)該直接訪問新地址。服務(wù)端會把新地址寫在?Location?頭部字段,方便客戶端使用。允許客戶端把 POST 請求修改為 GET。 |
| 304 | Not Modified | 請求的資源和之前的版本一樣,沒有發(fā)生改變。用來緩存資源,和條件性請求(conditional request)一起出現(xiàn) |
| 307 | Temporary Redirect | 目標(biāo)資源暫時性地移動到新的地址,客戶端需要去新地址進(jìn)行操作,但是不能修改請求的方法。 |
| 308 | Permanent Redirect | 和 301 類似,除了客戶端不能修改原請求的方法 |
| 400 | Bad Request | 客戶端發(fā)送的請求有錯誤(請求語法錯誤,body 數(shù)據(jù)格式有誤,body 缺少必須的字段等),導(dǎo)致服務(wù)端無法處理 |
| 401 | Unauthorized | 請求的資源需要認(rèn)證,客戶端沒有提供認(rèn)證信息或者認(rèn)證信息不正確 |
| 403 | Forbidden | 服務(wù)器端接收到并理解客戶端的請求,但是客戶端的權(quán)限不足。比如,普通用戶想操作只有管理員才有權(quán)限的資源。 |
| 404 | Not Found | 客戶端要訪問的資源不存在,鏈接失效或者客戶端偽造 URL 的時候回遇到這個情況 |
| 405 | Method Not Allowed | 服務(wù)端接收到了請求,而且要訪問的資源也存在,但是不支持對應(yīng)的方法。服務(wù)端必須返回?Allow?頭部,告訴客戶端哪些方法是允許的 |
| 415 | Unsupported Media Type | 服務(wù)端不支持客戶端請求的資源格式,一般是因為客戶端在?Content-Type?或者?Content-Encoding?中申明了希望的返回格式,但是服務(wù)端沒有實現(xiàn)。比如,客戶端希望收到?xml返回,但是服務(wù)端支持?Json |
| 429 | Too Many Requests | 客戶端在規(guī)定的時間里發(fā)送了太多請求,在進(jìn)行限流的時候會用到 |
| 500 | Internal Server Error | 服務(wù)器內(nèi)部錯誤,導(dǎo)致無法完成請求的內(nèi)容 |
| 503 | Service Unavailable | 服務(wù)器因為負(fù)載過高或者維護(hù),暫時無法提供服務(wù)。服務(wù)器端應(yīng)該返回?Retry-After?頭部,告訴客戶端過一段時間再來重試 |
上面這些狀態(tài)碼覆蓋了 API 設(shè)計中大部分的情況,如果對某個狀態(tài)碼不清楚或者希望查看更完整的列表,可以參考?HTTP Status Code?這個網(wǎng)站,或者?RFC7231 Response Status Codes?的內(nèi)容。
9. 錯誤處理:給出詳細(xì)的信息
如果出錯的話,在 response body 中通過?message?給出明確的信息。
比如客戶端發(fā)送的請求有錯誤,一般會返回?4XX Bad Request?結(jié)果。這個結(jié)果很模糊,給出錯誤 message 的話,能更好地讓客戶端知道具體哪里有問題,進(jìn)行快速修改。
-
如果請求的 JSON 數(shù)據(jù)無法解析,會返回?Problems parsing JSON
-
如果缺少必要的 filed,會返回?422 Unprocessable Entity,除了 message 之外,還通過?errors?給出了哪些 field 缺少了,能夠方便調(diào)用方快速排錯
基本的思路就是盡可能提供更準(zhǔn)確的錯誤信息:比如數(shù)據(jù)不是正確的 json,缺少必要的字段,字段的值不符合規(guī)定…… 而不是直接說“請求錯誤”之類的信息。
10. 驗證和授權(quán)
一般來說,讓任何人隨意訪問公開的 API 是不好的做法。驗證和授權(quán)是兩件事情:
-
驗證(Authentication)是為了確定用戶是其申明的身份,比如提供賬戶的密碼。不然的話,任何人偽造成其他身份(比如其他用戶或者管理員)是非常危險的
-
授權(quán)(Authorization)是為了保證用戶有對請求資源特定操作的權(quán)限。比如用戶的私人信息只能自己能訪問,其他人無法看到;有些特殊的操作只能管理員可以操作,其他用戶有只讀的權(quán)限等等
如果沒有通過驗證(提供的用戶名和密碼不匹配,token 不正確等),需要返回?401 Unauthorized狀態(tài)碼,并在 body 中說明具體的錯誤信息;而沒有被授權(quán)訪問的資源操作,需要返回?403 Forbidden?狀態(tài)碼,還有詳細(xì)的錯誤信息。
NOTE:Github API 對某些用戶未被授權(quán)訪問的資源操作返回?404 Not Found,目的是為了防止私有資源的泄露(比如黑客可以自動化試探用戶的私有資源,返回 403 的話,就等于告訴黑客用戶有這些私有的資源)。
11. 限流 rate limit
如果對訪問的次數(shù)不加控制,很可能會造成 API 被濫用,甚至被?DDos 攻擊。根據(jù)使用者不同的身份對其進(jìn)行限流,可以防止這些情況,減少服務(wù)器的壓力。
對用戶的請求限流之后,要有方法告訴用戶它的請求使用情況,Github API?使用的三個相關(guān)的頭部:
-
X-RateLimit-Limit: 用戶每個小時允許發(fā)送請求的最大值
-
X-RateLimit-Remaining:當(dāng)前時間窗口剩下的可用請求數(shù)目
-
X-RateLimit-Rest: 時間窗口重置的時候,到這個時間點可用的請求數(shù)量就會變成?X-RateLimit-Limit的值
如果允許沒有登錄的用戶使用 API(可以讓用戶試用),可以把?X-RateLimit-Limit?的值設(shè)置得很小,比如 Github 使用的?60。沒有登錄的用戶是按照請求的 IP 來確定的,而登錄的用戶按照認(rèn)證后的信息來確定身份。
對于超過流量的請求,可以返回?429 Too many requests?狀態(tài)碼,并附帶錯誤信息。而?Github API?返回的是?403 Forbidden,雖然沒有?429?更準(zhǔn)確,也是可以理解的。
Github 更進(jìn)一步,提供了不影響當(dāng)然?RateLimit?的請求查看當(dāng)前?RateLimit?的接口?GET /rate_limit。
12. Hypermedia API
Restful API 的設(shè)計最好做到 Hypermedia:在返回結(jié)果中提供相關(guān)資源的鏈接。這種設(shè)計也被稱為?HATEOAS。這樣做的好處是,用戶可以根據(jù)返回結(jié)果就能得到后續(xù)操作需要訪問的地址。
比如訪問?api.github.com,就可以看到 Github API 支持的資源操作。
13. 編寫優(yōu)秀的文檔
API 最終是給人使用的,不管是公司內(nèi)部,還是公開的 API 都是一樣。即使我們遵循了上面提到的所有規(guī)范,設(shè)計的 API 非常優(yōu)雅,用戶還是不知道怎么使用我們的 API。最后一步,但非常重要的一步是:為你的 API 編寫優(yōu)秀的文檔。
對每個請求以及返回的參數(shù)給出說明,最好給出一個詳細(xì)而完整地示例,提醒用戶需要注意的地方……反正目標(biāo)就是用戶可以根據(jù)你的文檔就能直接使用 API,而不是要發(fā)郵件給你,或者跑到你的座位上問你一堆問題。
參考資料
-
Github API v3
-
RESTful API 設(shè)計指南
-
REST接口設(shè)計規(guī)范
-
Restful API 首次被提出的論文:Architectural Styles and the Design of Network-based Software Architectures
總結(jié)
以上是生活随笔為你收集整理的跟着 Github 学习 Restful HTTP API 的优雅设计的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Cloud 微服务架构的五
- 下一篇: 如何使用 Redis 实现大规模的帖子浏