本篇文章中提到的技巧摘錄自 How to design better APIs。
原文中作者提出了 15 點 設計 REST API 的技巧,大部分是我認為非常有必要且重要的,因此引用了一部分原文中的內容並做了一些補充分享給大家。但這些技巧並不是硬性「規定」,可以根據團隊風格或者專案情況…等自行決定。
如果對 REST API 是什麼還並不了解的話,引用 淺談 REST API 的設計和規劃 – 大類的技術筆記 提到的這段簡單做個解釋:
保持一致
原文中提到了以下六點:
- 所有 fields, resources 和 parameters 的名稱應統一大小寫。
- 統一使用複數或者單數資源名稱,比如
/users/{id}
,/orders/{id}
或者/user/{id}
,/order/{id}
。 - 對所有 endpoints 使用相同的身分驗證和授權方法。
- 所有 API 使用相同的 HTTP Headers。
- 根據響應類型使用相同的 HTTP 狀態碼,例如找不到資源時使用
404
。 - 對相同類型的操作使用相同的 HTTP 方法,例如
DELETE
時。
補充
另外根據我工作上看到的例子,有幾點想補充:
- 任何名稱應統一命名規範。
- URI 命名應該引用作為「事物」(名詞)的資源而不是引用動作(動詞)。
以我工作上的例子來舉例,客戶相關的路由可以設計為:/client
/client/detail
/client/order
關於 REST API 的命名規範網路上有很多可以參考的文章,這邊就不延伸下去了。
使用 ISO 8601 UTC 標準處理時間
在處理日期和時間時,API 應始終返回 ISO 8601 標準的字串,並且日期應根據時區顯示。
{ "published_at": "2022-03-03T21:59:08Z" }
補充
JS 的 Date 有提供 toISOString()
API 來將時間轉為 ISO 8601 標準的字串:
const event = new Date(); console.log(event.toString()); // Sun Apr 24 2022 16:19:01 GMT+0800 (台北標準時間) console.log(event.toISOString()); // 2022-04-24T08:19:01.895Z
如果你的 project 使用的是 moment.js 來處理時間,也一樣可以 toISOString()
API 來將時間轉為 ISO 8601 標準的字串:
const time = moment().toISOString(); console.log(time); // 2022-04-24T08:19:01.895Z
要注意的是,如果使用 toISOString() 返回的都是 UTC 時間。
對公共接口進行例外處理
(此處的接口指的是 endpoints,也就是 GET /employee/{id}
, GET /client/detail
…等)
在默認情況下,每個接口都應該需要鑒權。但有些情況比如說:登入、註冊,是不需要鑒權的,就需要將它們設置成允許未經授權。
提供健康檢查接口
提供一個 GET /health
的接口用於判斷目前 API 服務是否正常運行。其他應用程序,例如負載平衡器(load balancers)可以調用此接口以在服務中斷時採取行動。
比如說:
app.get('/health', (req, res) => { res.setHeader('Content-Type', 'application/json'); res.send({ "message": "I'm still alive." }); });
版本化 API
確保對 API 進行版本控制並在每個請求中傳遞版本。API 版本可以使用 HTTP Headers 或查詢/路徑參數傳遞。即使是 API 的第一個版本(1.0) 也應該明確地進行版本控制。
一些例子:
- https://api.averagecompany.com/v1/health
- https://api.averagecompany.com/health?api_version=1.0
接受 API 密鑰認證
如果 API 需要由第三方調用時,則允許通過 API keys 來進行身分驗證。API keys 應使用自定義 HTTP Headers 傳遞,並且應該有一個有效日期,且可以隨時撤銷。同時應避免將 API keys 嵌入程式碼中,可以使用環境變數來代替(例如: process.env
)。
使用合理的 HTTP 狀態碼
使用傳統的 HTTP 狀態代碼來指示請求的成功或失敗,並在整個 API 中為相同的結果使用相同的狀態代碼。
舉一些例子:
200
成功201
成功創建400
來自客戶端的錯誤請求401
未經授權的請求403
缺少權限404
找不到資源429
過多請求5xx
內部錯誤(應不惜一切代價避免這些錯誤)
使用合理的 HTTP 方法
HTTP 方法有很多,但最重要的是:
- POST 用於創建資源
POST /users
- GET 用於獲取資源(單個資源和集合)
GET /users
GET /users/{id}
- PATCH 用於對資源部分更新
PATCH /users/{id}
- PUT 用於對資源完整更新(替換當前資源)
PUT /users/{id}
- DELETE用於刪除資源
DELETE /users/{id}
使用簡單且不言自明的名稱
大部分接口應面向資源命名,不要添加不必要的訊息。這同時也適用於 fields 的名稱。
✅好的例子如下:
GET /users
DELETE /users/{id}
POST /users/{id}/notification
user.first_name
order.number
❌糟糕的例子如下:
GET /getUser
POST /updateUser
POST /notification/user
order.ordernumber
user.firstName
使用標準化的錯誤響應
除了指示請求結果(成功或失敗)的HTTP狀態碼之外,在返回錯誤時,應使用標準化的錯誤響應,才能得知確切的錯誤信息從而找出錯誤原因。
// Request => GET /users/4TL011ax // Response <= 404 Not Found { "code": "user/not_found", "message": "A user with the ID 4TL011ax could not be found." } // Request => POST /users { "name": "John Doe" } // Response <= 400 Bad Request { "code": "user/email_required", "message": "The parameter [email] is required." }
POST 後返回創建的資源
在 POST
請求創建資源後返回創建的資源信息是非常有用的一步驟,因為有些情況下接下來的步驟會根據創建後的這個資源進行處理,所以會需要創建的資源的 id 或者其他信息。
// Request: POST /users { "email": "jdoe@averagecompany.com", "name": "John Doe" } // Response { "id": 1, "email": "jdoe@averagecompany.com", "name": "John Doe" }
以 PATCH 取代 PUT
PATCH
和 PUT
皆用於更新資源,但 PATCH
是對資源部分更新,而 PUT
是完全替換現有資源。
- 當使用
PUT
請求但只需要更新資源的一部分字段(fields)時,仍然需要傳遞所有的字段,這樣很容易出錯。 - 使用
PUT
即允許任何字段不受任何限制地更新,這也是非常危險的一件事。 - 根據經驗,在實際應用中幾乎不存在任何對資源進行完整更新的有意義的用例。
補充
舉個例子,假設今天有一個使用者資訊如下:
{ id: 1 first_name: 'John', last_name: 'Dog', age: 20 }
若我只是想修改 last_name
時,使用 PUT
我需要將所有字段傳回去:
{ id: 1 first_name: 'John', last_name: 'Doe', age: 20 }
而如果使用 PATCH
,我只需要回傳 last_name
就可以進行更新:
{ last_name: 'Doe', }
響應結果過多時使用分頁
當響應結果過多時可以適當使用分頁。比如說以 page_number
和 page_size
…等來控制要獲取的結果內容。
// Request => GET /users?page_number=1&page_size=15 // Response <= 200 OK { "page_number": 1, "page_size": 15, "count": 378, "data": [ // resources ], "total_pages": 26, "has_previous_page": true, "has_next_page": true }
補充
我比較常遇到的例子是用 offset
和 limit
,offset 用於指定從哪開始,limit 用於指定一次響應多少個結果。
例如:
http://localhost:8080/api/clients/list?offset=1&limit=10
代表從第一個結果開始返回十個結果。