REST API設計實踐
1 RESTful
1.1 什麼是RESTful
官方解釋
Representational State Transfer 的簡稱,即 表現層狀態轉移。
人看的解釋
- REST指一組架構約束條件和原則, 如果一個架構符合 REST 的約束條件和原則,就稱之為 RESTful 架構
- RESTful 是一種軟體架構風格,而不是標準
大神總結
- URL定位資源
- HTTP動詞(GET, POST, DELETE, PUT)描述操作
- HTTP狀態碼錶示響應結果
總的來說,RESTful是一種web服務設計風格,風格意思就是大家預設的但不是強制的。
1.2 REST規範
用URL定位資源
REST 的主體是資源,所謂“資源”,就是網路上的一個具體資訊,例如:一張圖片,一段文字、一種服務。總之就是一個實際存在的東西,而 URL 就是用來指向這個資源的。
例如:
https://api.example.com/users
用HTTP動詞描述操作
對資源的操作,無外乎 CRUD(增刪改查),RESTful 中,每個 HTTP 動詞對應一個 CRUD 操作。
- GET:對應 Retrieve 操作。GET請求從不改變資源的狀態。無副作用。GET方法是冪等的。GET方法具有隻讀的含義。因此,你可以完美的使用快取。
- POST:對應 Create 操作
- DELETE:對應 Delete 操作
- PUT:對應 Update 操作
用HTTP狀態碼錶示響應結果
RESTful Web服務應使用合適的HTTP狀態碼來響應客戶端請求
- 2xx - 成功 - 一切都很好
- 4xx - 客戶端錯誤 - 如果客戶端發生錯誤(例如客戶端傳送無效請求或未被授權)
- 5xx – 伺服器錯誤 - 如果伺服器發生錯誤(例如,嘗試處理請求時出錯)
2 RESTful API設計實踐
設計RESTful API可能很棘手,因為沒有官方和強制標準。基本上,實現API的方法有很多種,但其中一些方法已在實踐中得到證明並被廣泛採用。這篇文章涵蓋了構建 HTTP 和 RESTful API 的最佳實踐。我們將討論 URL 結構、HTTP 方法、建立和更新資源、設計關係、有效負載格式、分頁、版本控制等等。
2.1 URL設計建議
用名詞代替動詞表示資源
這讓你的API更簡潔,URL數目更少
正確示例:
GET /employees
GET /employees?state=external
POST /employees
PUT /employees/56
錯誤示例:
/getAllEmployees
/getAllExternalEmployees
/createEmployee
/updateEmployee
資源用複數表示,全部小寫,用_或-連線,不要使用駝峰命名法
這是個人愛好問題,但複數形式更為常見。最重要的是:避免複數和單數名詞混合使用,這顯得非常混亂且容易出錯。
推薦:
/employees
/employees/21
不推薦:
/employee
/employee/21
每個資源使用兩個URL
資源集合用一個URL,具體某個資源用一個URL
/employees #資源集合的URL
/employees/56 #具體某個資源的URL
在URL中強制加入版本號
從始至終,都使用版本號釋出您的RESTful API。將版本號放在URL中以是必需的。如果您有不相容和破壞性的更改,版本號將讓你能更容易的釋出API。釋出新API時,只需在增加版本號中的數字。這樣的話,客戶端可以自如的遷移到新API,不會因呼叫完全不同的新API而陷入困境。使用直觀的 “v” 字首來表示後面的數字是版本號。
你不需要使用次級版本號(“v1.2”),因為你不應該頻繁的去釋出API版本。
正確示例:
/v1/employees
錯誤示例:
/v1.2/employees
提供分頁資訊
一次性返回資料庫所有資源不是一個好主意。因此,需要提供分頁機制。通常使用資料庫中眾所周知的引數offset和limit。
/employees?offset=30&limit=15 #返回30 到 45的員工
如果客戶端沒有傳這些引數,則應使用預設值。通常預設值是offset = 0和limit = 10。如果資料庫檢索很慢,應當減小limit值
/employees #返回0 到 10的員工
此外,如果您使用分頁,客戶端需要知道資源總數。例: 請求:
GET /employees
響應:
{
"offset": 0,
"limit": 10,
"total": 3465,
"employees": [
//...
]
}
對可選的、複雜的引數,使用查詢字串(?)
為了讓你的URL更小、更簡潔。為資源設定一個基本URL,將可選的、複雜的引數用查詢字串表示。
正確示例:
GET /employees?state=internal&maturity=senior
錯誤示例:
GET /employees
GET /externalEmployees
GET /internalEmployees
GET /internalAndSeniorEmployees
非資源請求用動詞
有時API呼叫並不涉及資源(如計算,翻譯或轉換)。
GET /translate?from=de_DE&to=en_US&text=Hallo
//Trigger an operation that changes the server-side state
POST /restartServer
//no body
POST /banUserFromChannel
{ "user": "123", "channel": "serious-chat-channel" }
在這種情況下,不涉及任何資源,伺服器執行一個操作並將結果返回給客戶端。因此,您應該在 URL 中使用動詞而不是名詞來清楚地區分操作(RPC-style APIs)和用於領域建模的資源(REST APIs)。
- 如果一個API已操作為主,應該設計成RPC風格
- 如果一個API主要用來對相關資料進行CRUD,應該設計成REST風格
2.2 HTTP動詞設計建議
HTTP動詞語義
- Get
- 冪等
- 只讀。GET永遠不會改變伺服器端資源的狀態,它必須沒有副作用。
- PUT
- 冪等
- 可用於建立和更新
- 常用於更新(完全更新)。例如:PUT /employees/1- 更新員工 1
- 始終在請求中包含整個有效負載。要麼全有要麼全無。PUT不用於部分更新
- POST
- 非冪等
- 用於建立。例如:POST /employees建立一個新員工
- DELETE
- 冪等
- 用於刪除。例如:DELETE /employees/1
- PATCH
- 冪等
- 用於部分更新。例如:PATCH /employees/1- 使用負載中包含的欄位更新員工1, 員工1的其他欄位不會被更新
2個URL乘以4個HTTP方法就是一組很好的功能。看看這個表格:
POST(建立) | DELETE(刪除) | PUT(更新) | GET(查詢) | |
---|---|---|---|---|
/employees | 建立一個新員工 | 刪除所有員工 | 批量更新員工資訊 | 列出所有員工 |
/employees/56 | (錯誤) | 刪除56號員工 | 更新56號員工的資訊 | 獲取56號員工的資訊 |
2.3 HTTP狀態碼設計建議
RESTful Web服務應使用合適的HTTP狀態碼來響應客戶端請求,其中的大部分HTTP狀態碼都不會被用到,只會用其中的一小部分。通常會用到一下幾個:
2xx:成功 | 3xx:重定向 | 4xx:客戶端錯誤 | 5xx:伺服器錯誤 |
---|---|---|---|
200 成功 | 301 永久重定向 | 400 錯誤請求 | 500 內部伺服器錯誤 |
201 建立 | 304 資源未修改 | 401未授權 | |
403 禁止 | |||
404 未找到 |
2.4 響應內容建議
使用小駝峰命名法
使用小駝峰命名法作為屬性識別符號。
{ "yearOfBirth": 1982 }
不要使用下劃線(year_of_birth)或大駝峰命名法(YearOfBirth)。通常,RESTful Web服務將被JavaScript編寫的客戶端使用。客戶端會將JSON響應轉換為JavaScript物件(通過呼叫var person = JSON.parse(response)),然後呼叫其屬性。因此,最好遵循JavaScript程式碼通用規範
將實際資料包裝在一個data欄位中
GET響應,PUT、POST 和 PATCH 請求體都應該將實際資料包裝data欄位
- 有剩餘空間可以新增元資料(例如用於分頁、連結、棄用警告、錯誤訊息)
- 保持資料一致性
- 與 JSON:API 標準相容
GET /employees # 返回data欄位中的物件列表
{
"data": [
{ "id": 1, "name": "Larry" }
, { "id": 2, "name": "Peter" }
]
}
GET /employees/1 # 返回data欄位中的單個物件
{
"data": {
"id": 1,
"name": "Larry"
}
}
返回有用的錯誤資訊
除了合適的狀態碼之外,還應該在HTTP響應正文中提供有用的錯誤提示和詳細的描述。這是一個例子:
請求
GET /employees?state=super
響應
// 400 Bad Request
{
"errors": [
{
"status": 400,
"detail": "Invalid state. Valid values are 'internal' or 'external'",
"code": 352,
"links": {
"about": "http://www.domain.com/rest/errorcode/352"
}
}
]
}
提供用於導航的連結
理想情況下,您不要讓您的客戶構建 URL 以使用您的 REST API。讓我們考慮一個例子。
客戶想要訪問員工的工資報表。因此,他必須知道他可以通過將查詢引數附加salaryStatements到員工 URL(例如/employees/21/salaryStatements)來訪問工資報表。這種字串連線容易出錯、脆弱且難以維護。如果您更改訪問 REST API 中的薪水報表的方式(例如,現在使用“salary-statements”或“paySlips”),所有客戶端都將中斷。
{
"data": [
{
"id":1,
"name":"Paul",
"links": [
{
"salary": "http://www.domain.com/employees/1/salaryStatements"
}
]
}
]
}
如果客戶端完全依賴連結來獲取工資單,那麼即使您更改 API,他也不會中斷,因為客戶端將始終獲得有效的 URL(只要您在 URL 更改的情況下更新連結)。另一個好處是您的 API 變得更具自我描述性,並且客戶不必經常查詢文件。
適當的設計關係
讓我們假設每個employee都有 amanager和幾個teamMembers。在API中設計關係基本上有三個常用方式:連結、外來鍵和嵌入。
它們都是有效的,正確的選擇取決於用例。主要考慮
- 客戶端的訪問模式
- 可容忍的請求次數
- 有效負載大小
連結:負載小,但要獲取完整資料訪問次數過多
{
"data": [
{
"id": 1,
"name": "Larry",
"relationships": {
"manager": "http://www.domain.com/employees/1/manager",
"teamMembers": [
"http://www.domain.com/employees/12",
"http://www.domain.com/employees/13"
]
//or "teamMembers": "http://www.domain.com/employees/1/teamMembers"
}
}
]
}
外來鍵:負載大小適中,沒有重複,但使用者需要解析資料,使用麻煩
{
"data": [
{
"id": 1,
"name": "Larry",
"relationships": {
"manager": 5 ,
"teamMembers": [ 12, 13 ]
}
}
],
"included": {
"manager": {
"id": 5,
"name": "Kevin"
},
"teamMembers": [
{ "id": 12, "name": "Albert" }
, { "id": 13, "name": "Tom" }
]
}
}
嵌入:使用者獲取資料最方便,但負載過大,引用資料會重複
{
"data": [
{
"id": 1,
"name": "Larry",
"manager": {
"id": 5,
"name": "Kevin"
},
"teamMembers": [
{ "id": 12, "name": "Albert" }
, { "id": 13, "name": "Tom" }
]
}
]
}