PCA 架構之旅 06 — REST API 設計
拆好服務後,下一步是定義它們的對外契約。API 是服務之間最重要的介面,一旦發佈就很難改了,設計當下的每個決定,往後 3 年都得跟著它走。
PCA 不會考 REST vs GraphQL 的哲學問題,但會考實務議題:版本化、冪等、錯誤處理、鑑權、速率限制。這些東西上線後再改要死一堆人。
這是 PCA 架構之旅 的第六步。上一篇 05 · Microservices 拆分。
為什麼這一步重要
在 PCA case study 題目中怎麼出現
PCA 題目常給你一段「使用者 call API 失敗時應該重試」「一筆訂單不能重複建立」等敘述,要你選合適的 API 設計與 GCP 服務(例如 API Gateway、Cloud Endpoints、Apigee)。答錯通常是因為沒搞懂冪等性(idempotency)與重試的關係。
考生常見錯誤
- 把動詞寫進 URL:
/getOrder、/createOrder不符合 REST 資源化原則。 - 沒有版本策略:API 一出去就凍結,未來要改只能全部換。
- 錯誤碼全部用 500:客戶無法區分是使用者錯還是系統錯,導致重試風暴。
核心概念
Google AIP(API Improvement Proposals)基本原則
| 原則 | 說明 |
|---|---|
| 資源導向 | URL 是名詞,不是動詞。POST /orders 不是 /createOrder |
| 標準動詞 | GET / POST / PATCH / DELETE / LIST 各有語意 |
| 版本化 | /v1/orders,breaking change 上 v2 |
| 冪等性 | PUT 與 DELETE 天生冪等;POST 要靠 idempotency key |
| 一致的錯誤格式 | 用 RFC 7807 或 Google Error Model |
冪等性(Idempotency)
同樣的請求送 N 次,結果都一樣。為什麼要在意這個?因為網路會斷、客戶會重試,沒做冪等保護就很容易跑出兩筆訂單或重複扣款。
實作方式:客戶端帶一個 Idempotency-Key header(UUID),伺服器端用 Cloud Memorystore 或資料庫記錄已處理的 key,後續相同 key 直接回傳上次結果。
GCP API 閘道選擇
| 產品 | 適用 | 特色 |
|---|---|---|
| Cloud Endpoints | 基本 API 管理 | 整合 Cloud Run/GKE、OpenAPI 支援 |
| API Gateway | 全託管 serverless API | 設定最簡單 |
| Apigee | 企業級 API 管理 | 有 developer portal、分析、貨幣化 |
思考框架
為每個 API 問自己:
- 這個動作的主體資源是什麼? URL 就圍著它走。
- 這個動作是否冪等? 不冪等就必須有保護機制。
- 失敗時客戶要知道什麼? 錯誤訊息要能讓客戶「知道該不該重試」。
- 誰能呼叫? 匿名、登入使用者、內部服務、合作夥伴,各走不同鑑權。
- 有沒有速率限制? 沒設就等著被 DDoS 或內部誤用打爆。
走一遍範例 — 登雲書店
先從 3 個最關鍵的服務示範:order-service、cart-service、partner-ingest-service。
order-service API
URL 命名(v1)
POST /v1/orders 建立訂單
GET /v1/orders/{orderId} 查詢單一訂單
GET /v1/orders?buyer={id}&state=paid 列出訂單
PATCH /v1/orders/{orderId}:cancel 取消訂單(命名動作)
建立訂單 request:
POST /v1/orders HTTP/1.1
Authorization: Bearer <token>
Idempotency-Key: 0f9b4e2a-3c1d-4a22-bb9a-9a1b0f0f0f0f
Content-Type: application/json
{
"buyer_id": "usr_48211",
"items": [
{ "sku": "BK-9789571234567", "quantity": 1 },
{ "sku": "BK-9789867891230", "quantity": 2 }
],
"shipping_address_id": "addr_902"
}
成功回應:
201 Created
Location: /v1/orders/ord_20260415_0001
{
"order_id": "ord_20260415_0001",
"state": "pending_payment",
"total_amount": { "currency": "TWD", "value": 1380 }
}
錯誤格式(Google Error Model 風格):
{
"error": {
"code": 409,
"status": "ALREADY_EXISTS",
"message": "Order with this idempotency key already exists",
"details": [
{ "@type": "type.googleapis.com/OrderConflict", "existingOrderId": "ord_20260415_0001" }
]
}
}
cart-service API
GET /v1/carts/{cartId}
POST /v1/carts/{cartId}/items
PATCH /v1/carts/{cartId}/items/{itemId}
DELETE /v1/carts/{cartId}/items/{itemId}
POST /v1/carts/{cartId}:checkout → 呼叫 order-service
購物車項目增刪沒有 idempotency-key 沒關係,但 :checkout 必須有,否則網路重送會重複下單。
partner-ingest-service API
因為是大批次、非即時,走非同步模式:
POST /v1/ingestJobs
→ 202 Accepted
{ "job_id": "job_20260415_tienshia_01", "status": "queued" }
GET /v1/ingestJobs/{jobId}
→ 回傳 status: queued | running | succeeded | failed + 失敗列表 URL
CSV 檔本身用 signed URL 直接傳到 Cloud Storage,這樣大檔案就不會卡在 API gateway。
共用設計決策
| 面向 | 決策 |
|---|---|
| 版本 | URL 路徑 /v1/、breaking change 時推 /v2/,保留 v1 12 個月 |
| 鑑權 | 外部 API 走 OIDC + API key;內部服務間走 Workload Identity |
| 速率限制 | Apigee quota policy:消費者 100 req/min、合作夥伴 20 req/min |
| 錯誤處理 | 4xx 不重試、5xx 指數退避重試(最多 5 次)、429 遵守 Retry-After |
| 分頁 | pageSize + pageToken(Google 風格,避免 offset 深分頁問題) |
常見陷阱
- 回 200 但 body 寫
{error: ...}:HTTP 狀態碼與業務錯誤分離,CDN、LB 會誤判。 - 只有一個 generic error:客戶無法程式化處理,該用明確錯誤碼與結構化 detail。
- 把 query 塞太多狀態:
/orders?state=paid&buyer=X&from=...&to=...&...,超過 5–6 個 query 就要考慮POST /orders:search。 - 破壞性變更不升版:move field、change type 都是 breaking,客戶會炸。
延伸閱讀
- Google AIP — API Improvement Proposals — Google 內部的 API 設計標準,完整且實用。
- Google Cloud APIs Design Guide — GCP 官方 API 設計指南,AIP 的摘要版。
- RFC 9457(取代 RFC 7807)— Problem Details for HTTP APIs — 標準化的錯誤回應格式(RFC 9457 於 2023 年發佈,正式取代 RFC 7807)。
- Stripe API Idempotency — 業界公認冪等設計的最佳範本。
下一步:API 定義好了,接著就得決定每個資源該放哪種儲存,這就是下一篇 儲存特性分析。
🎯 換你練習
理論讀完,換自己來。到 架構師設計工作坊 · 步驟 6 填入你的 case study,邊寫邊內化。