Restful API 設計最佳實踐
Restful API 成熟度
在 Richardson Maturity Model 模型中,將 RESTful 分爲 4 個等級:
4 個等級分別是:
第二級(Level 1)的 Web 服務引入了資源的概念。每個資源有對應的標識符和表達。
第三級(Level 2)的 Web 服務使用不同的 HTTP 方法來進行不同的操作,並且使用 HTTP 狀態碼來表示不同的結果。如 HTTP GET 方法來獲取資源,HTTP DELETE 方法來刪除資源。
第四級(Level 3)的 Web 服務使用 HATEOAS。在資源的表達中包含了鏈接信息。客戶端可以根據鏈接來發現可以執行的動作。
實踐 1:一類資源兩個 URL
一個 URL 表示該類型資源集合,另一個 URL 用來表示特定的資源元素。
1# 資源集合:
2/epics
3# 資源元素:
4/epics/5
實踐 2:使用一致的複數名詞
避免混用複數和單數形式,只應該使用統一的複數名詞來表達資源。
反例:
1GET /story
2GET /story/3
正例:
1GET /stories
2GET /stories/3
實踐 3:使用名詞而不是動詞
使用 Http 方法來表達動作(增、刪、改、查):
-
增(POST:非冪等性): 使用 POST 方法創建新的資源。
-
刪(DELETE:冪等性): 使用 DELETE 方法刪除存在的資源。
-
改(PUT:冪等性): 使用 PUT 或 PATCH 方法來更新已存在的資源。
-
查: 使用 GET 方法讀取資源。(GET:冪等性)
反例:
1/getAllEpics
2/getAllFinishedEpics
3/createEpic
4/updateEpic
正例:
1GET /epics
2GET /epics?state=finished
3POST /epics
4PUT /epics/5
實踐 4:將實際數據包裝在 data 字段中
GET /epics 在數據字段中返回 epic 資源列表:
1{
2 "data": [
3 { "id": 1, "name": "epic1" }
4 , { "id": 2, "name": "epic2" }
5 ]
6}
GET /epic/1 在數據字段中返回 id 爲 1 的 epic 對象:
1{
2 "data": {
3 "id": 1,
4 "name": "epic1"
5 }
6}
PUT,POST 和 PATCH 請求的有效負荷還應包含實際對象的數據字段。
優點:
-
還有空間擴展元數據
-
一致性
-
兼容 JSON API 標準
實踐 5:對可選及複雜參數使用查詢字符串(?)
反例:
1GET /employees
2GET /externalEmployees
3GET /internalEmployees
4GET /internalAndSeniorEmployees
保持 URL 簡單短小。堅持使用基本 URL,將複雜或可選參數移動到查詢字符串。
1GET /employees?state=internal&title=senior
2GET /employees?id=1,2
另外還可以使用 JSON API 方式過濾:
1GET /employees?filter[state]=internal&filter[title]=senior
2GET /employees?filter[id]=1,2
實踐 6:使用 HTTP 狀態碼
RESTful Web 服務應使用合適的 HTTP 狀態碼來響應客戶端的請求。
-
2xx - 成功 - 一切正常。
-
4xx - 客戶端錯誤 - 如果客戶端的故障(例如:客戶端發送無效請求或未經授權)
-
5xx - 服務器錯誤 - 服務端的故障(嘗試處理請求時的錯誤,如數據庫故障,依賴服務不可用,編碼錯誤或不應發生的狀態)
請注意,使用所有過多的 HTTP 狀態碼可能會讓 API 用戶感到困惑。所以應該保持使用精簡的 HTTP 狀態碼集。常用狀態碼如下:
-
2xx:成功,操作被成功接收並處理
-
200:請求成功。一般用於 GET 與 POST 請求
-
201:已創建。成功請求並創建了新的資源
-
3xx:重定向,需要進一步的操作以完成請求
-
301:永久移動。請求的資源已被永久的移動到新 URI,返回信息會包括新的 URI,瀏覽器會自動定向到新 URI。今後任何新的請求都應使用新的 URI 代替
-
304:未修改。所請求的資源未修改,服務器返回此狀態碼時,不會返回任何資源。客戶端通常會緩存訪問過的資源,通過提供一個頭信息指出客戶端希望只返回在指定日期之後修改的資源
-
4xx:客戶端錯誤,請求包含語法錯誤或無法完成請求
-
400:客戶端請求的語法錯誤,服務器無法理解
-
401:請求要求用戶的身份認證
-
403:服務器理解請求客戶端的請求,但是拒絕執行此請求
-
404:服務器無法根據客戶端的請求找到資源(網頁)。通過此代碼,網站設計人員可設置” 您所請求的資源無法找到” 的個性頁面
-
410:客戶端請求的資源已經不存在。410 不同於 404,如果資源以前有現在被永久刪除了可使用 410 代碼,網站設計人員可通過 301 代碼指定資源的新位置
-
5xx:服務器錯誤,服務器在處理請求的過程中發生了錯誤
-
500:服務器內部錯誤,無法完成請求
不要過度使用 404。狀態碼的使用要儘量精確。如果資源可用,但禁止用戶訪問,則返回 403。如果資源曾經存在但現已被刪除或停用,請使用 410。
實踐 7:提供有用的錯誤消息
除了提供恰當的 HTTP 狀態代碼外,還應該在 HTTP 響應正文中提供有用且詳細的錯誤描述。如下所示:
請求:
1GET /epics?state=unknow
響應:
1// 400 Bad Request
2{
3 "errors": [
4 {
5 "status": 400,
6 "detail": "Invalid state. Valid values are 'biz' or 'tech'",
7 "code": 352,
8 "links": {
9 "about": "http://www.jira.com/rest/errorcode/352"
10 }
11 }
12 ]
13}
實踐 8:使用 HATEOAS
HATEOAS 是 Hypermedia As The Engine Of Application State 的縮寫,從字面上理解是 “超媒體即是應用狀態引擎” 。其原則就是客戶端與服務器的交互完全由超媒體動態提供,客戶端無需事先了解如何與數據或者服務器交互。相反的,在一些 RPC 服務或者 Redis,Mysql 等軟件,需要事先了解接口定義或者特定的交互語法。舉例如下:
客戶想要訪問 epic 的用戶故事清單。因此,他必須知道他可以通過將查詢參數 stories 附加到員工 URL(例如 / epics/21/stories)來訪問用戶故事清單。這種字符串拼接易錯,脆弱且難以維護。如果更改了在 REST API 中訪問 salary 語句的方式(例如,現在使用 “storyStatements” 或“userStories”),則所有客戶端都將中斷。
更好的做法是在響應中提供客戶可以跟進的鏈接。例如,對 GET /epic 的響應可能如下所示:
1{
2 "data": [
3 {
4 "id":1,
5 "name":"epic1",
6 "links": [
7 {
8 "story": "http://www.domain.com/epics/21/stories"
9 }
10 ]
11 }
12 ]
13}
優點:
-
如果 API 被更改,客戶端依舊會獲取有效的 URL(只要保證在 URL 更改時更新鏈接)。
-
API 變得更具自描述性,客戶端不必經常查找文檔。
實踐 9:恰當地設計關係
假設每個 story 都有一個 epic 和幾個 sub task。在 API 中設計關係基本上有三種常用選項:鏈接,側載和嵌入。
它們都是有效的,正確的選擇取決於用例。基本上,應根據客戶端的訪問模式以及可容忍的請求數量和有效負載大小來設計關係。
鏈接
1{
2 "data": [
3 {
4 "id": 1,
5 "name": "用戶故事1",
6 "relationships": {
7 "epic": "http://www.domain.com/story/1/epic",
8 "subTasks": [
9 "http://www.domain.com/subTasks/12",
10 "http://www.domain.com/subTasks/13"
11 ]
12 //or "subTasks": "http://www.domain.com/story/1/subTasks"
13 }
14 }
15 ]
16}
-
有效負載小。
-
許多請求。
-
客戶端必須將數據拼接在一起才能獲得所有數據。
側載
1{
2 "data": [
3 {
4 "id": 1,
5 "name": "用戶故事1",
6 "relationships": {
7 "epic": 5 ,
8 "subTask": [ 12, 13 ]
9 }
10 }
11 ],
12 "included": {
13 "epic": {
14 "id": 5,
15 "name": "epic5"
16 },
17 "subTasks": [
18 { "id": 12, "name": "子任務12" }
19 , { "id": 13, "name": "子任務13" }
20 ]
21 }
22}
客戶端還可以通過諸如GET /stories?include=epic,subTasks
之類的查詢參數來控制側載實體。
-
一次請求。
-
定製的有效載荷大小。沒有重複(例如,即使被許多用戶故事引用,也只用提供一次 epic)
-
客戶端仍然必須將數據拼接在一起以便解決關係,這可能非常麻煩。
嵌入
1{
2 "data": [
3 {
4 "id": 1,
5 "name": "用戶故事1",
6 "epic": {
7 "id": 5,
8 "name": "epic5"
9 },
10 "subTask": [
11 { "id": 12, "name": "子任務12" }
12 , { "id": 13, "name": "子任務13" }
13 ]
14 }
15 ]
16}
-
對客戶來說最方便。是可以直接通過關係來獲取實際數據。
-
如果客戶端不需要關係,白白加載關係。
-
有效負載大小和重複增加。可能多次嵌入引用的實體。
實踐 10:使用小駝峯命名法來命名屬性
1{
2 "epic.dateOfCreated": 2019-05-16
3}
1// 反例
2epic.created_date // 違反JavaScript規範
3epic.DateOfCreated // 建議用於構造方法
4
5// 正例
6epic.dateOfCreated
實踐 11:使用動詞進行操作
有時對 API 調用的響應不涉及資源(如計算,轉義或變換)。例:
1// 讀取
2GET /translate?from=de_DE&to=en_US&text=Hallo
3GET /calculate?para2=23¶2=432
4
5// 觸發更改服務器端狀態的操作
6POST /restartServer
7// 無消息體
8
9POST /banUserFromChannel
10{ "user": "123", "channel": "serious-chat-channel" }
通過動詞來表達 RPC 風格 API,它比嘗試 RESTful 風格來進行操作更簡單,更直觀(例如 PATCH / server with {“restart”:true})。REST 風格非常適合與領域模型交互,RPC 適合於操作。更多信息請查看 “Understanding RPC Vs REST For HTTP APIs”。
實踐 12:分頁
兩種流行的分頁方法是:
-
基於偏移的分頁
-
基於鍵集的分頁,又稱繼續令牌,也稱爲光標(推薦)
基於偏移的分頁
一般方法是使用參數 offset 和 limit 來進行分頁:
1# 返回30至45的epics
2/epics?offset=30&limit=15
如果未填參數,則可使用默認值(offset=0, limit=100 ):
1# 返回0至100的epics
2/epics
還可以在響應數據中,提供前一頁和後一頁的鏈接:
請求:
1# 返回30至45的epics
2/epics?offset=30&limit=15
響應:
1{
2 "pagination": {
3 "offset": 20,
4 "limit": 10,
5 "total": 3465,
6 },
7 "data": [
8 //...
9 ],
10 "links": {
11 "next": "http://www.domain.com/epics?offset=30&limit=10",
12 "prev": "http://www.domain.com/epics?offset=10&limit=10"
13 }
14}
基於偏移量的分頁實現很簡單,但是有兩個缺點:
-
查詢慢。數據量大時 SQL 偏移子句執行會很慢。
-
不安全。分頁期間的變更。
基於鍵集的分頁,又稱繼續令牌,也稱爲光標(推薦)
簡單來說就是使用索引列來進行分頁。假設 epic 有一個索引列 data_created,我們就可以使用 data_created 來分頁。
1GET /epics?pageSize=100
2# 客戶端接受最靠前的100條epic信息,使用`data_created`字段排序
3# 該分頁最老epic的`dataCreated` 字段值爲 1504224000000 (= Sep 1, 2017 12:00:00 AM)
4
5GET /epics?pageSize=100&createdSince=1504224000000
6# 客戶端請求1504224000000之後的100個epics數據。
7# 該分頁最前面的epic創建於1506816000000.
該分頁方式解決了基於偏移的分頁的許多缺點,但對調用方來說不太方便。
更好的方式是通過向日期添加附加信息(如 id)來創建所謂的 continuation token,以提高可靠性和效率。此外,應該向該令牌的有效負載中提供專用字段,以便客戶端不用必須通過查看元素才能搞清楚。甚至還可以進一步提供下一頁鏈接。
因此 GET /epics?pageSize=100
請求將返回如下:
1{
2 "pagination": {
3 "continuationToken": "1504224000000_10",
4 },
5 "data": [
6 // ...
7 // last element:
8 { "id": 10, "dateCreated": 1504224000000 }
9 ],
10 "links": {
11 "next": "http://www.domain.com/epics?pageSize=100&continue=1504224000000_10"
12 }
13}
下一頁鏈接使 API 真正成爲 RESTful 風格,因爲客戶端只需通過這些鏈接(HATEOAS)即可查看集合。無需手動構建 URL。此外,服務端可以簡單地更改 URL 結構而不會破壞客戶端,保證接口的演進性。
實踐 13:確保 API 的可演進性
避免破壞性變更
-
保持向後兼容。只要客戶端能接受就通過添加字段的方式。
-
複製和棄用。要更改現有字段(重命名或更改結構),可在該字段旁邊添加新字段,並在接口手冊中棄用該字段。一段時間後,刪除舊字段。
-
利用超媒體和 HATEOAS。只要客戶端使用響應中的鏈接來訪問(且不會手動創建 URL),即可以安全地更改 URL 而不會破壞客戶端。
-
使用新名稱創建新資源。如果新業務需求導致全新的領域模型和工作流,則可以創建新資源。
保持業務邏輯在服務側
不要讓服務成爲轉儲數據訪問層,它通過直接公開數據庫模型(低級 API)來提供 CRUD 功能。這造成了高耦合。
因此,我們應該構建高層次 / 基於工作流的 API 而不是低級 API。
實踐 14:版本化
API 實在無法演進,則必須提供不同版本的 API。版本控制允許在不破壞客戶端的情況下,在新版本中發佈不兼容和重大更改的 API。
有兩種最流行的版本控制方法:
-
通過 URLs 版本化
-
通過 Accept HTTP Header 進行版本控制(內容協商)
通過 URLs 版本化
只需將 API 的版本號放在每個資源的 URL 中即可。
1/v1/epics
優點:
-
對 API 開發人員非常簡單。
-
對客戶端訪問也非常簡單。
-
可以複製和粘貼 URL。
缺點:
-
非 RESTful。(該方式會令 URL 發生變化)
-
破壞 URLs。客戶端必須維護和更新 URL。
由於其簡單性,該方式被各大廠商廣泛使用,例如:Facebook, Twitter, Google/YouTube, Bing, Dropbox, Tumblr 以及 Disqus 等。
通過 Accept HTTP Header 進行版本控制(內容協商)
更 RESTFul 的方式是利用通過 Accept HTTP 請求頭的內容協商。
1GET /epics
2Accept: application/vnd.myapi.v2+json
優點:
-
URLs 保持不變
-
RESTFul 方式
-
HATEOAS 友好
缺點:
-
稍微難以使用。客戶必須注意標題。
-
無法再複製和粘貼網址。
source: //kaelzhang81.github.io/2019/05/24/Restful-API 設計最佳實踐
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/HePvXzEB5tg57cUfFkcUVA