REST API 設計 是做什麼的?
API 就是產品:消費者的心智模型,而非你的資料庫結構,決定了 API 的形狀。這項技能要防止的代價高昂的錯誤,是推出不一致的 API 表面——混亂的命名、錯誤的狀態碼、在持續成長的資料表上使用 offset 分頁——每個消費者都會因此寫死應對邏輯,一旦整合就幾乎無法修復。
操作流程
請依此順序進行。資源先於方法,因為方法的語義取決於資源是什麼;錯誤與分頁合約應在撰寫規格前確定,因為事後補救會破壞消費者端相容性。
步驟 1:收集輸入
先收集再設計。將任何假設標記為「猜測」。
- 消費者:內部服務、第一方客戶端或外部開發者。外部消費者會提高一致性標準——若未知則預設為外部。
- 領域的核心名詞以及哪些名詞擁有哪些名詞(影響巢狀結構)。
- 預期的集合大小與成長趨勢。可能超過 ~10,000 筆記錄的列表,排除 offset 分頁。
- 每個端點的讀寫比例與延遲期望。
- 驗證模型(影響 401 與 403 的正確性)。
步驟 2:命名資源
- 複數名詞:/users、/orders、/products。
- 一個層級的擁有關係巢狀:/users/{id}/orders。巢狀不要超過兩層——改用過濾器扁平化(/orders?user_id=... 優於 /users/{id}/orders/{oid}/items/{iid})。
- 路徑中不要出現動詞;HTTP 方法就是動詞。對於無法對應 CRUD 動詞的實際動作(例如發送發票),以子資源 POST 建模:POST /invoices/{id}/send-attempts。
- 統一大寫規則:多詞路徑段落使用 kebab-case,JSON 欄位使用 snake_case——選定後一致套用。
步驟 3:指派方法與狀態碼
- GET:讀取,等冪,無請求主體。回傳 200 加上主體。
- POST:建立。回傳 201 加上指向新資源的 Location 標頭。在主體中回傳建立的表示,讓客戶端無需額外 GET。
- PUT:完整取代,等冪。缺少的欄位意味著「清除」,而非「保留」。
- PATCH:部分更新(JSON Merge Patch 或 JSON Patch——在規格中說明使用哪一種)。
- DELETE:刪除,等冪,回傳 204。刪除已刪除的資源時,回傳 204 或 404——選一個並記錄下來。
- 錯誤:400 格式錯誤輸入、401 缺少/無效憑證、403 有效憑證但無權限、404 資源不存在(當揭露存在性是洩漏時,使用 404 而非 403)、409 狀態衝突、422 格式正確但語意無效的輸入、429 速率限制(附上 Retry-After)、500 伺服器錯誤——絕不暴露堆疊追蹤或內部識別碼。
步驟 4:確定錯誤合約
所有錯誤使用同一形狀,機器可讀的錯誤碼優先:
{
"error": "validation_error",
"message": "Email is required",
"field": "email",
"request_id": "req_abc123"
}
request_id 必須在伺服端記錄,以便支援人員進行關聯。human message 供開發者查看日誌,絕不用於終端使用者顯示。
步驟 5:確定分頁與過濾
- 任何會成長的集合使用 cursor-based 分頁:回傳一個不透明 cursor 和 has_more 旗標。Offset 分頁僅可接受用於小型、受控、面向管理員的列表——它會線性退化,並在並行寫入下跳過/重複資料列。
- 預設頁面大小 25,最大值 100。超過最大值的頁面大小回傳 400 拒絕,不要默默截斷。
- 透過查詢參數進行過濾與排序:?status=active&sort=created_at:desc。紀錄哪些欄位可過濾;對未知的過濾參數回傳 400 而非忽略(默默忽略會隱藏客戶端錯誤)。
步驟 6:套用效能與酬載預算
- p95 延遲預算:互動式端點 300 毫秒,報表/匯出 1 秒。若操作無法在 1 秒內完成,則改為非同步:202 Accepted 加上一個可輪詢的狀態資源。
- 回應酬載預算:典型回應保持在 100 KB 以下;任何經常超過 1 MB 的回應需要分頁、欄位選擇(?fields=)或專用的匯出流程。
- 在 URL 路徑中標示重大變更版本(/v1/)。新增性變更(新的可選欄位)不需要版本提升。棄用時間表則交給 api-versioning-strategist。
對比範例
錯誤:
POST /getUserOrders?userId=42&page=3
→ 200 { "data": [...], "error": null }
路徑中有動詞,POST 卻有 GET 的語義,offset 分頁,200 包裝一個錯誤欄位,讓客戶端必須解析主體才知道呼叫是否失敗。
正確:
GET /users/42/orders?status=shipped&limit=25&cursor=eyJpZCI6OTh9
→ 200 { "orders": [...], "next_cursor": "eyJpZCI6MTIzfQ", "has_more": true }
→ 404 { "error": "not_found", "message": "User 42 does not exist", "request_id": "req_9f2" }
名詞路徑、擁有關係巢狀、cursor 分頁,狀態碼承載結果。
交付物
產出一個端點規格,包含:資源表(路徑、方法、每個方法的狀態碼)、共用錯誤形狀、分頁合約(cursor 格式、預設值、最大值)、過濾白名單,以及每個端點的延遲/酬載預算。
請勿
- 不要在 API 中鏡像資料庫結構——消費者會與你日後想更改的內部實作耦合。
- 不要用 200 回傳失敗並附帶錯誤主體;客戶端和監控系統都依賴狀態碼。
- 不要在無限制的集合上使用 offset 分頁——它會在寫入時重複和跳過資料列,並線性退化。
- 不要讓路徑巢狀超過兩層;深巢狀只編碼一種存取路徑,排除了所有其他方式。
- 不要默默忽略未知的查詢參數——拼錯的過濾器回傳未過濾的資料,是資料外洩等級的錯誤。
品質標準
- 消費者能夠根據這份規格實作客戶端,無需提出任何問題。
- 每個端點都說明其狀態碼,包括錯誤情況。
- 所有集合端點共用一個分頁合約;所有錯誤共用一個形狀。
- 相同的概念在所有路徑和酬載中使用相同的名稱。
- 每個端點都有明確的延遲預算,超過 1 秒的端點為非同步。