Restful API 設計最佳實踐

Restful API 成熟度

在 Richardson Maturity Model 模型中,將 RESTful 分爲 4 個等級:

4 個等級分別是:

第一級(Level 0)的 Web 服務僅使用 HTTP 作爲傳輸方式,實際上只是遠程方法調用(RPC)的一種具體形式。SOAP 和 XML-RPC 都屬於此類。

第二級(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 方法來表達動作(增、刪、改、查):

  1. 增(POST:非冪等性): 使用 POST 方法創建新的資源。

  2. 刪(DELETE:冪等性): 使用 DELETE 方法刪除存在的資源。

  3. 改(PUT:冪等性): 使用 PUT 或 PATCH 方法來更新已存在的資源。

  4. 查: 使用 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 請求的有效負荷還應包含實際對象的數據字段。

優點:

實踐 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 狀態碼來響應客戶端的請求。

請注意,使用所有過多的 HTTP 狀態碼可能會讓 API 用戶感到困惑。所以應該保持使用精簡的 HTTP 狀態碼集。常用狀態碼如下:

不要過度使用 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}

優點:

  1. 如果 API 被更改,客戶端依舊會獲取有效的 URL(只要保證在 URL 更改時更新鏈接)。

  2. 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之類的查詢參數來控制側載實體。

嵌入

 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:分頁

兩種流行的分頁方法是:

  1. 基於偏移的分頁

  2. 基於鍵集的分頁,又稱繼續令牌,也稱爲光標(推薦)

基於偏移的分頁

一般方法是使用參數 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}

基於偏移量的分頁實現很簡單,但是有兩個缺點:

  1. 查詢慢。數據量大時 SQL 偏移子句執行會很慢。

  2. 不安全。分頁期間的變更。

基於鍵集的分頁,又稱繼續令牌,也稱爲光標(推薦)

簡單來說就是使用索引列來進行分頁。假設 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 的可演進性

避免破壞性變更

保持業務邏輯在服務側

不要讓服務成爲轉儲數據訪問層,它通過直接公開數據庫模型(低級 API)來提供 CRUD 功能。這造成了高耦合。

因此,我們應該構建高層次 / 基於工作流的 API 而不是低級 API。

實踐 14:版本化

API 實在無法演進,則必須提供不同版本的 API。版本控制允許在不破壞客戶端的情況下,在新版本中發佈不兼容和重大更改的 API。

有兩種最流行的版本控制方法:

通過 URLs 版本化

只需將 API 的版本號放在每個資源的 URL 中即可。

1/v1/epics

優點:

缺點:

由於其簡單性,該方式被各大廠商廣泛使用,例如:Facebook, Twitter, Google/YouTube, Bing, Dropbox, Tumblr 以及 Disqus 等。

通過 Accept HTTP Header 進行版本控制(內容協商)

更 RESTFul 的方式是利用通過 Accept HTTP 請求頭的內容協商。

1GET /epics
2Accept: application/vnd.myapi.v2+json

優點:

缺點:

source: //kaelzhang81.github.io/2019/05/24/Restful-API 設計最佳實踐

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/HePvXzEB5tg57cUfFkcUVA