跳至主要內容
ESC
pca-architect-journey — 第 6/13 篇

PCA 架構之旅 06 — REST API 設計

PCA

拆好服務後,下一步是定義它們的對外契約。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 問自己:

  1. 這個動作的主體資源是什麼? URL 就圍著它走。
  2. 這個動作是否冪等? 不冪等就必須有保護機制。
  3. 失敗時客戶要知道什麼? 錯誤訊息要能讓客戶「知道該不該重試」。
  4. 誰能呼叫? 匿名、登入使用者、內部服務、合作夥伴,各走不同鑑權。
  5. 有沒有速率限制? 沒設就等著被 DDoS 或內部誤用打爆。

走一遍範例 — 登雲書店

先從 3 個最關鍵的服務示範:order-servicecart-servicepartner-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,客戶會炸。

延伸閱讀


下一步:API 定義好了,接著就得決定每個資源該放哪種儲存,這就是下一篇 儲存特性分析

🎯 換你練習

理論讀完,換自己來。到 架構師設計工作坊 · 步驟 6 填入你的 case study,邊寫邊內化。

pca-architect-journey — 6/13 完成 查看系列全覽 →

留言討論

徽章解鎖!