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

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

內文字級

PCA 雲端架構師之旅 06 — REST API 設計
更新:2026-10-12

服務邊界畫好後,還要決定它們怎麼對外提供資料。API 一旦有人串接,路徑、欄位和錯誤碼就不能隨意更動;設計時多花一點時間,後面改版會容易得多。

例如訂單 API 的欄位名稱一改,外部合作夥伴可能還在送舊格式。這時不是把舊欄位刪掉就好,還得考慮版本、過渡期和相容處理。

這篇會從重試、版本、錯誤回應和存取控制幾個實際問題切入。它是 PCA 雲端架構師之旅 的第六步;上一篇是 05 · Microservices 拆分。

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

圖解:客戶端沒收到回應時會重送 POST;兩次請求若帶同一把 idempotency key,伺服器可辨識它們是同一次操作,避免重複建立訂單。前提是查重與建立訂單有一致的保護,不能只在寫入前查一下 key。


網路逾時後,重送會發生什麼?

假設客戶端送出訂單後逾時了:請求可能根本沒到伺服器,也可能訂單已建立,只是回應沒送回來。客戶端無法只憑逾時判斷是哪一種,因此需要能安全重試的設計。

這就是冪等性(idempotency)要處理的問題:同一個操作重送,不應再建立第二筆訂單。重試機制與去重必須一起設計;光說「失敗就再送一次」不夠。

除了重試,API 上線後還常遇到三件事:

  • 把動詞塞進 URL,寫成 /getOrder、/createOrder,這不符合 REST 把資源當名詞的精神。
  • 沒有版本或相容策略,欄位一改就影響既有客戶端。
  • 錯誤全部回 500,客戶端分不出該修正請求還是稍後重試。
查看流程與節點文字

箭頭表示流程方向;方括號與引號內是節點名稱,可搭配下方圖解閱讀。

sequenceDiagram
  participant C as Client
  participant A as Order API
  participant D as Order DB(含唯一 key 紀錄)
  C->>A: POST /orders + Key K1
  A->>D: 以 K1 原子建立或查詢訂單
  alt 第一次請求
    D-->>A: 建立 O1 並提交交易
    A-->>C: 201 O1
  else 逾時後重送
    D-->>A: K1 已對應 O1
    A-->>C: 回傳既有結果,不重做副作用
  end
以唯一鍵約束和交易把去重與建立訂單綁在一起,才能避免兩個同時抵達的請求都通過「先查再寫」。若還要呼叫付款等外部系統,也須替下游設計相應的冪等保護。

先把資源、版本和重試說清楚

Google 的 AIP(API Improvement Proposals)整理了不少 API 設計慣例。這裡先抓幾項會影響客戶端使用方式的原則:

原則說明
資源導向URL 是名詞,不是動詞。POST /orders 不是 /createOrder
方法語意GET 讀取、POST 建立或執行動作、PATCH 更新、DELETE 刪除;List 通常用 GET 讀取集合
版本化/v1/orders,breaking change 上 v2
冪等性PUT、DELETE 應有冪等語意;POST 依操作設計重試保護
一致的錯誤格式用 RFC 9457(RFC 7807 的後繼版)或 Google Error Model

表格超出欄寬時,可左右捲動;鍵盤可聚焦表格後用左右方向鍵查看。

這裡最容易誤會的是冪等性。它說的是同一個操作重複執行後,預期的伺服器狀態與執行一次相同;每次 HTTP 回應不一定逐字相同。

網路逾時時,客戶端可能會重送請求。若結帳 API 沒有處理重複操作,使用者按第二次就可能建立兩筆訂單。HTTP 方法的語意能幫忙判斷風險,但實作仍要遵守那些語意,不能以為用了 PUT 或 DELETE 就自動安全。

對會建立訂單或付款的 POST,可以讓客戶端帶 Idempotency-Key。伺服器用唯一鍵與交易,讓同一個 key 只對應一次建立操作,後續重送再讀取既有結果。單純「查不到 key 就繼續執行」不夠:兩個請求可能同時查不到,仍各做一次。付款若交給外部服務,還要把同一操作的去重需求帶到下游。(參考:Stripe 的冪等請求說明、RFC 9110 的 HTTP 方法語意)

📝 考場提點

讀到重試或重複投遞,先問「同一操作再跑一次會不會多做一筆?」GET、PUT、DELETE 有各自的 HTTP 語意,但仍要檢查實作;POST 也不是一律只能靠 idempotency key,可以依資源 ID、唯一約束或交易設計重試保護。

REST API 以 orders 資源比較 GET、PUT、PATCH、DELETE 與 POST 的語意、冪等性和安全重試方式,並整理狀態碼處理

URL 先表達資源,HTTP 方法再表達動作。GET、PUT 與 DELETE 具冪等語意;POST 若可能因重送造成重複建立,就要另外設計去重。狀態碼也要讓客戶端能判斷該修正請求,還是依 Retry-After 或退避策略稍後重試。


三種 API 管理方式,怎麼比較?

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

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

表格超出欄寬時,可左右捲動;鍵盤可聚焦表格後用左右方向鍵查看。

如果只需要替幾個服務加上基本驗證和流量管理,先釐清 API Gateway 或 Cloud Endpoints 是否已足夠。Apigee 提供更多 API 產品管理能力,也意味著不同的設定和費用;別只因功能最多就選它。

例如要經營 developer portal、替外部開發者提供產品方案和分析資料,才有理由進一步評估 Apigee。若是內部服務或單一對外 API,先列出鑑權、配額和營運要求,再選擇較合適的管理方式。

查看流程與節點文字

箭頭表示流程方向;方括號與引號內是節點名稱,可搭配下方圖解閱讀。

flowchart TB
  R[API 管理需求] --> P{需要 developer portal<br/>貨幣化或進階分析?}
  P -- 是 --> A[Apigee]
  P -- 否 --> S{主要是 serverless API<br/>且希望全託管?}
  S -- 是 --> G[API Gateway]
  S -- 否 --> E{需要 OpenAPI 與<br/>Cloud Run/GKE<br/>基本管理?}
  E -- 是 --> C[Cloud Endpoints]
  E -- 否 --> N[先確認是否真的<br/>需要閘道]
  A --> V[再設定驗證、配額與監控]
  G --> V
  C --> V
Apigee 的企業能力要有明確需求才值得付費;一般 serverless 或基本 OpenAPI 管理,使用較輕量的方案即可。

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

設計 API 前,可以先問這五個問題:

  1. 這個動作的主體資源是什麼? 找到它,URL 就圍著它走,自然就不會冒出動詞。
  2. 重送會改變結果嗎? 若會,確認如何避免重複建立、扣款或通知。
  3. 失敗的時候,客戶端需要知道什麼? 重點不是訊息寫得多漂亮,是讓對方「知道該不該重試」。
  4. 誰能呼叫它? 匿名訪客、登入使用者、內部服務、外部合作夥伴,鑑權方式天差地遠。
  5. 需要怎樣的流量保護? 估計正常尖峰,並限制濫用或錯誤重試對後端的影響。

這五題不一定一次就能答完,但把未知條件記下來,比 API 上線後才發現客戶端無法安全重試容易處理。

查看流程與節點文字

箭頭表示流程方向;方括號與引號內是節點名稱,可搭配下方圖解閱讀。

flowchart TB
  U[使用者要完成的動作] --> R[找出主體資源與 URL]
  R --> H[選 HTTP 方法與狀態碼]
  H --> I{操作有不可重複的副作用?}
  I -- 是 --> K[加入 idempotency key]
  I -- 否 --> E[維持方法原生語意]
  K --> A[定義呼叫者身分與權限]
  E --> A
  A --> Q[設定 quota/rate limit]
  Q --> F[定義可判斷是否重試的<br/>錯誤格式]
先從資源和方法建立契約,再依副作用補冪等,最後統一鑑權、限流與結構化錯誤,避免每支 API 各自發明規則。

走一遍範例 — 登雲書店

用登雲書店的訂單、購物車與書商匯入流程,看看不同 API 該怎麼定義資源、重試和錯誤。其他服務可以沿用判斷方式,但不必照搬同一組路徑。

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

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

最後的 :cancel 表示取消訂單這個動作,不只是修改一個欄位。依 Google AIP 的 custom method 慣例,會改變資料的命名動作用 POST,路徑在資源名稱後接冒號與動詞;只有標準方法不足以表達意圖時才需要這樣設計。

建立訂單的請求長這樣,注意那個 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 }
}

若同一位呼叫者用相同 key 重送相同請求,API 應回傳前一次保存的結果,不再建立訂單。另一種情況才是衝突:同一個 key 被拿來送不同的訂單內容。這時可回 409,提醒客戶端不能把新操作當成舊操作重試:

{
  "error": {
    "code": 409,
    "status": "ALREADY_EXISTS",
    "message": "This idempotency key was used with different order details. Use a new key.",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "IDEMPOTENCY_KEY_REUSED",
        "domain": "api.cloudon-books.example"
      }
    ]
  }
}

這個範例沿用 Google API 錯誤模型 的 JSON 結構:code 是 HTTP 狀態碼,status 是標準狀態,details 裡的 ErrorInfo.reason 則讓客戶端辨認具體原因。程式應依這些欄位處理錯誤,不要靠解析可能修改的 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

購物車操作也不能一概說「重送沒關係」。如果 POST 的意思是「加一件」,重送可能變成加兩件;若改用明確的品項 ID 和設定數量的操作,重試就比較容易控制。最後的 :checkout 風險更高:它會建立訂單,甚至觸發付款,必須讓同一次結帳在重送後仍只產生一筆業務結果。

先列出各操作重送一次會發生什麼,再決定用 idempotency key、固定資源 ID、唯一約束或其他方式保護;方法名稱本身不會替你完成這件事。

partner-ingest-service:批次處理不讓請求一直等

合作夥伴上傳書目資料後,檢查與匯入可能要跑上一段時間。API 可以先回傳工作 ID,讓客戶端查進度或等待通知;不必讓一次 HTTP 請求等到整批處理完:

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 閘道、也不會吃掉服務的記憶體和連線。閘道只負責收「請幫我處理這個檔」的指令,不負責搬運檔案本身。這個「控制流跟資料流分開」的設計,適合大檔上傳與長時間處理的情境。

📝 考場提點

大檔上傳和長時間處理不一定要綁在同一個同步請求裡。可以讓 API 建立工作並回傳 job ID,檔案則由客戶端透過 signed URL 上傳。是否要用這種方式,還得看檔案大小、處理時間、授權與失敗後的重試需求。

大檔批次匯入把控制流與資料流分開:API 快速回傳 job_id,大檔經 signed URL 直傳 Cloud Storage,再由背景 Worker 處理

控制流只負責建立工作並回傳 202 Accepted 與 job_id;資料流讓大檔透過 signed URL 直傳 Cloud Storage,再由事件或佇列觸發背景處理。客戶端另外查狀態,不把長工作綁在同一條 HTTP 連線上。

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

每個服務若各訂一套版本和錯誤格式,串接者會很難維護。下面是假設登雲書店與合作夥伴協調後,訂出的一組 API 契約範例:

面向決策
版本URL 路徑 /v1/、breaking change 時推 /v2/,保留 v1 12 個月
鑑權外部 API 走 OIDC + API key;內部服務間以各服務自己的身分驗證(GKE 上以 Workload Identity Federation for GKE 取得身分)
速率限制Apigee quota policy:消費者 100 req/min、合作夥伴 20 req/min
錯誤處理先分辨可恢復錯誤;429 遵守 Retry-After,可安全重試時才對 5xx 退避重送
分頁pageSize + pageToken(Google 風格,避免 offset 深分頁問題)

表格超出欄寬時,可左右捲動;鍵盤可聚焦表格後用左右方向鍵查看。

這裡有兩項值得另外說明。登雲書店的內部服務若跑在 GKE,可用 Workload Identity Federation for GKE 取得身分,避免把長期金鑰放進服務。分頁則選 pageToken,讓服務端能以穩定的排序鍵延續查詢;它比深度 offset 更容易控制效能,但仍要設計好排序與資料更新時的行為,不能保證永遠不會漏掉或重複。

查看流程與節點文字

箭頭表示流程方向;方括號與引號內是節點名稱,可搭配下方圖解閱讀。

flowchart TB
  E[收到 HTTP 回應] --> S{狀態碼類型}
  S -- 2xx --> O[成功,不重試]
  S -- 4xx --> C{是 429?}
  C -- 否 --> F[修正請求或權限<br/>不要盲目重試]
  C -- 是 --> R[遵守 Retry-After]
  S -- 5xx --> B[指數退避+隨機抖動]
  R --> I{操作具冪等保護?}
  B --> I
  I -- 否 --> X[停止自動重試<br/>避免重複副作用]
  I -- 是 --> T[在重試上限內再送]
狀態碼必須讓客戶端知道下一步;只有暫時性錯誤且操作具備冪等保護時,才適合自動重試。

上線後才痛的那幾個雷

下面這些都不是設計階段會自爆的問題,而是上線後、流量上來、客戶接多了才慢慢咬人的——也是設計 API 時值得先想清楚的點:

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

延伸閱讀


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

🎯 換你練習

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

PCA 雲端架構師之旅 — 第 6 篇/共 13 篇 查看系列全覽 →

讀完後,留一個學習紀錄

試著用自己的話說出一個重點,以及你會如何運用。實作文章請先核對結果與清理資源,再標記為讀完。

文章進度以你確認讀完的標記為準,可隨時取消。舊版造訪紀錄保留,但不計入讀完;這份自評不代表已掌握內容。

留言討論

文章目錄與系列

徽章解鎖!