跳至主要內容
ESC
PCA 雲端架構師之旅 — 第 6/13 篇

PCA 雲端架構師之旅 06 — REST API 設計

PCA 雲端架構師之旅 06 — REST API 設計
Updated: 2026-07-17

服務拆好了,接下來要定義它們對外講話的方式。API 就是服務之間的那紙合約——一旦發佈出去、有人開始接,你就被綁死了。當下隨手定的命名、隨手回的錯誤碼,往後 3 年都得拖著走。

我看過太多團隊在這裡吃虧。系統架構畫得漂漂亮亮,結果 API 是工程師趕著上線時邊寫邊定的,半年後想改一個欄位名稱,發現外部串接的合作夥伴一改就炸,只好硬著頭皮維護兩套邏輯。改 API 的痛,永遠來得比你想像中早。

PCA 不會考你 REST 跟 GraphQL 哪個比較好這種哲學題,它考的是上線後真的會咬人的東西:版本怎麼升、冪等怎麼保、錯誤怎麼回、誰能呼叫、流量怎麼擋。這是 PCA 雲端架構師之旅 的第六步,上一篇是 05 · Microservices 拆分

同一個 POST 請求因逾時重送兩次,API 以相同 idempotency key 只建立一筆訂單的流程圖

圖解:客戶端沒收到回應時會重送 POST;兩次請求若帶同一把 idempotency key,伺服器只建立第一筆訂單,第二次直接回傳既有結果。沒有這把鑰匙,一次網路逾時就可能變成兩筆訂單、兩次扣款。


題目其實在偷偷考你冪等

PCA 的 case study 很愛在敘述裡埋一句「使用者 call API 失敗時應該重試」「同一筆訂單不能重複建立」,然後問你該怎麼設計 API、配哪個 GCP 服務(通常選項裡會有 API Gateway、Cloud Endpoints、Apigee)。

這種題目答錯,幾乎都是栽在同一個地方:沒搞懂冪等性(idempotency)跟重試是綁在一起的。題目講「失敗要重試」,潛台詞就是「那你重試會不會重複下單?」——它在考的是冪等,不是重試本身。看懂這層,這類題目你會答得很穩。

另外有三個錯誤是考生反覆在犯的,先擺出來,後面我們一個個拆:

  • 把動詞塞進 URL,寫成 /getOrder/createOrder,這不符合 REST 把資源當名詞的精神。
  • 完全沒有版本策略,API 一出去就等於凍結,哪天要改只能整批換掉。
  • 錯誤全部回 500,客戶端根本分不出是自己傳錯還是伺服器掛了,分不出來就只好一直重試,重試風暴就是這樣養出來的。

Google 的 API 設計就那幾條規矩

GCP 自己的 API 都照一套叫 AIP(API Improvement Proposals)的規範在走。你不用把每條 AIP 背起來,但底下這幾條原則是 PCA 等級該有的肌肉記憶:

原則說明
資源導向URL 是名詞,不是動詞。POST /orders 不是 /createOrder
標準動詞GET / POST / PATCH / DELETE / LIST 各有語意
版本化/v1/orders,breaking change 上 v2
冪等性PUT 與 DELETE 天生冪等;POST 要靠 idempotency key
一致的錯誤格式用 RFC 9457(RFC 7807 的後繼版)或 Google Error Model

這張表裡最值得單獨講的是冪等性。所謂冪等,就是同一個請求送一次跟送十次,結果都一樣。

為什麼非在意它不可?因為網路一定會斷、客戶端一定會重試,這是你管不到的事實。沒做冪等保護,使用者點一次「結帳」轉圈圈以為失敗了又點一次,你後台就生出兩筆訂單、扣兩次款。這不是假設,是金流服務每天都在防的事。

做法其實不複雜:客戶端在請求帶一個 Idempotency-Key header(通常是 UUID),伺服器收到後先查這個 key 有沒有處理過——可以記在 Cloud Memorystore,也可以直接落資料庫——處理過就把上次的結果原封不動回給它,別再執行一次。Stripe 那套就是這麼幹的,業界很多人直接抄它的設計。

📝 考場提點

看到題目寫「客戶端會重試」「請求可能重複送達」「at-least-once 投遞」,腦中要立刻亮一盞燈:這題在考冪等。GET / PUT / DELETE 天生冪等,POST 不是——所以建立資源(下單、付款、發送)這類 POST 才需要 idempotency key。考場上別把「重試」跟「冪等」當兩件事看,它們在題目裡幾乎是同義詞。


三個閘道,別每題都選 Apigee

API 設計完,前面通常要擺一層閘道。GCP 給你三個選項,差別很實際:

產品適用特色
Cloud Endpoints基本 API 管理整合 Cloud Run/GKE、OpenAPI 支援
API Gateway全託管 serverless API設定最簡單
Apigee企業級 API 管理有 developer portal、分析、貨幣化

過來人的提醒:很多人一看到「API 管理」就反射選 Apigee,因為它最豪華。但 Apigee 是企業級重武器,貴、設定也重,案子裡如果只是要把幾個 Cloud Run 服務包一層、加個 API key 驗證,用它根本是高射砲打蚊子。

判斷的關鍵字其實藏在題目裡:題目提到 developer portal、把 API 當產品賣(貨幣化)、要給外部開發者完整的分析報表——這些字眼出現,才輪到 Apigee。如果只是內部服務之間或單純對外開個 serverless API,API Gateway 或 Cloud Endpoints 就夠了,還省錢。


每個 API,先問自己這五件事

設計一支 API 之前,這幾個問題我每次都會過一遍,考場上也是用它快速收斂答案:

  1. 這個動作的主體資源是什麼? 找到它,URL 就圍著它走,自然就不會冒出動詞。
  2. 這個動作冪等嗎? 不冪等,就一定要配保護機制,沒有商量。
  3. 失敗的時候,客戶端需要知道什麼? 重點不是訊息寫得多漂亮,是讓對方「知道該不該重試」。
  4. 誰能呼叫它? 匿名訪客、登入使用者、內部服務、外部合作夥伴,鑑權方式天差地遠。
  5. 要不要限流? 不設速率限制,等著被外部 DDoS 或內部某個寫壞的迴圈打爆。

這五題不是考試專用。真要設計上線系統,少問哪一題都會在事後付代價——通常是凌晨三點被 on-call 叫醒的那種代價。


走一遍範例 — 登雲書店

老規矩,拿登雲書店來實際走一遍。先挑三個最關鍵的服務示範:order-servicecart-servicepartner-ingest-service,其他幾個套同樣的邏輯就好。

order-service:訂單服務怎麼定 URL

POST   /v1/orders                    建立訂單
GET    /v1/orders/{orderId}          查詢單一訂單
GET    /v1/orders?buyer={id}&state=paid  列出訂單
PATCH  /v1/orders/{orderId}:cancel   取消訂單(命名動作)

注意最後那個 :cancel。取消訂單不是建一個新資源、也不是單純改個欄位,它是一個「動作」。AIP 風格遇到這種真的塞不進標準 CRUD 的操作,會用冒號加動詞的「命名動作(custom method)」來表達,而不是退回去寫 /cancelOrder

建立訂單的請求長這樣,注意那個 Idempotency-Key

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,並用 Location header 告訴客戶端新訂單在哪:

201 Created
Location: /v1/orders/ord_20260415_0001
{
  "order_id": "ord_20260415_0001",
  "state": "pending_payment",
  "total_amount": { "currency": "TWD", "value": 1380 }
}

如果同一個 idempotency key 又送一次呢?這時候就靠前面講的機制接住它——回一個結構化的衝突錯誤,順便把上次那筆訂單的 ID 附上去,客戶端拿到就知道「喔,這筆其實已經成立了」:

{
  "error": {
    "code": 409,
    "status": "ALREADY_EXISTS",
    "message": "Order with this idempotency key already exists",
    "details": [
      { "@type": "type.googleapis.com/OrderConflict", "existingOrderId": "ord_20260415_0001" }
    ]
  }
}

這就是 Google Error Model 的長相:一個固定的 error 物件,裡面有數字碼、字串狀態、人看的訊息,外加一個 details 陣列裝結構化資訊。重點在它穩定——客戶端可以靠 status 寫程式判斷,不用去 parse 那串 message 文字。

cart-service:購物車不全都要冪等

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 不一樣——它會去呼叫 order-service 真的下單,網路一抖重送一次就是兩筆訂單、兩次扣款。所以 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

收到請求先回 202 Accepted,意思是「我收下了,排隊處理中」,給一個 job_id;客戶端事後拿這個 id 去 poll 進度。

還有一個細節值得學起來:CSV 檔本身別走 API。讓客戶端拿 signed URL 直接把檔案丟進 Cloud Storage,幾百 MB 的大檔就不會卡在 API 閘道、也不會吃掉服務的記憶體和連線。閘道只負責收「請幫我處理這個檔」的指令,不負責搬運檔案本身。這個「控制流跟資料流分開」的設計,在 case study 裡是很討喜的答案。

📝 考場提點

題目只要出現「大檔上傳」「批次匯入」「報表匯出」「處理要好幾分鐘」這類字眼,標準答案的方向是兩個:第一,改成非同步(先回 202 + job id,再讓對方 poll 狀態),別讓請求同步卡住;第二,大檔走 Cloud Storage signed URL 直傳,別硬塞進 API。這兩招幾乎是 GCP 題目的固定套路,看到關鍵字直接往這邊靠。

把共用的設計決策一次定清楚

每個服務各寫各的版本、各定各的鑑權,遲早一團亂。這幾項該在團隊層級統一拍板:

面向決策
版本URL 路徑 /v1/、breaking change 時推 /v2/,保留 v1 12 個月
鑑權外部 API 走 OIDC + API key;內部服務間走 Workload Identity Federation for GKE
速率限制Apigee quota policy:消費者 100 req/min、合作夥伴 20 req/min
錯誤處理4xx 不重試、5xx 指數退避重試(最多 5 次)、429 遵守 Retry-After
分頁pageSize + pageToken(Google 風格,避免 offset 深分頁問題)

這張表裡有兩個地方常被問到,順手說一下。內部服務之間的鑑權走 Workload Identity Federation for GKE(舊稱 Workload Identity),不要在服務裡塞長期金鑰——這是 GCP 一貫的主張,case study 看到「服務間呼叫如何驗證」幾乎都是往這個方向答。分頁用 pageToken 而不是 offset,是因為資料量一大、offset 翻到很後面會越查越慢,還可能因為中間有人插入新資料而漏掉或重複;token-based 分頁就沒這問題。


上線後才痛的那幾個雷

下面這些都不是設計階段會自爆的問題,而是上線後、流量上來、客戶接多了才慢慢咬人的——也正是 PCA 愛考的點:

  • HTTP 回 200,body 裡卻寫 {"error": ...} 業務錯誤跟 HTTP 狀態碼是兩回事,但別把它們攪在一起。前面的 CDN、Load Balancer、監控系統全看狀態碼決定要不要重試、要不要報警,你回 200 它們就以為一切正常,錯誤被吞得無聲無息。
  • 整支 API 只有一種籠統的錯誤。 客戶端沒辦法用程式判斷該怎麼處理,只能人工看 log。該給的是明確的錯誤碼加上結構化的 details,讓對方能寫 if-else 接住。
  • query string 塞太多狀態。 /orders?state=paid&buyer=X&from=...&to=...&... 一旦條件超過五六個,就該認真考慮改成 POST /orders:search,把查詢條件放進 body,URL 也乾淨。
  • 破壞性變更卻不升版。 改欄位名、搬欄位位置、改型別,這些對接你 API 的人來說全是地雷。breaking change 就老老實實上 /v2/,舊版留一段過渡期,別在 v1 上面動刀。

延伸閱讀


下一步:API 的對外契約定好了,接著就要決定每個資源該落在哪種儲存——這正是下一篇 儲存特性分析 的主題。

🎯 換你練習

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

PCA 雲端架構師之旅 — 6/13 完成 查看系列全覽 →

留言討論

徽章解鎖!