API 設計 FAQ(REST,以及什麼時候不要用它)

後端 更新於 Oct 9, 2026
章節優先度優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值優先度 3/5 — 值得會 — 多半是必備模式的變形值得會優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問冷門只標在真正需要的章節上 —— 沒標的是背景/參考資料。

範圍 — 設計一支 HTTP API:資源建模、狀態碼、冪等、分頁、版本管理、錯誤格式、認證授權,以及 REST 和 GraphQL、gRPC 的比較。 另見:authentication.md — session 與 JWT; be_programming_notes.md — 限流、重試、 程式裡的冪等鍵;web_long_connections.md — 串流與推播;webhook_integration.md — 可靠地 接收 webhook。

「幫 X 設計一支 API」是多數後端面試的暖身題。真正被評分的是:你有沒有在想這份 契約 —— 它怎麼演進、它會怎麼壞,以及客戶端要做什麼才能正確地用它。


1) 資源,不是動詞 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

REST 的資源是一個帶著穩定 URI 的名詞;動詞交給 HTTP method。

text
GET    /orders?status=open&limit=50   list
POST   /orders                        create           → 201 + Location
GET    /orders/1042                   read one
PUT    /orders/1042                   replace whole
PATCH  /orders/1042                   partial update
DELETE /orders/1042                   remove           → 204
POST   /orders/1042/cancel            a state transition that isn't CRUD

在 review 裡站得住腳的規則:名詞用複數、小寫加連字號、最多只巢狀一層 (/orders/1042/items,再深就改成給連結)、路徑裡不要有動詞 (/getOrders ❌)—— 除非那是真正的動作、而不是對資源的修改 (/cancel、/search、/reindex),這種大家都接受寫成 POST 的子資源。

Method 的語意 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

Method 安全(沒有副作用) 冪等(呼叫 N 次 == 1 次) 可快取
GET ✅ ✅ ✅
HEAD ✅ ✅ ✅
PUT ❌ ✅ ❌
DELETE ❌ ✅(第二次呼叫回 404/204) ❌
PATCH ❌ ⚠️ 只有你刻意這樣設計才是 ❌
POST ❌ ❌ 很少

冪等就是那個讓重試變安全的性質,而網路讓重試無可避免。既然 POST 不是冪等的, 就給它一個冪等鍵:

http
POST /payments
Idempotency-Key: 7f1c9e2a-...

伺服器把這個 key 連同第一次的回應存起來,之後任何重複請求都重播那份回應 —— 這個模式在 be_programming_notes.md 裡寫得很完整, AI 工具呼叫的版本則在 llm_tool_idempotency.md。


2) 狀態碼 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

代碼 用在
200 OK 成功,有回應內容
201 Created 建立了資源 —— 回一個 Location header
202 Accepted 非同步:已接受待處理,還沒完成
204 No Content 成功但沒有內容(DELETE,或不回東西的 PUT)
400 Bad Request 語法錯誤/驗證失敗
401 Unauthorized 沒有通過認證(憑證錯誤或缺少)
403 Forbidden 認證過了,但沒有權限
404 Not Found 沒有這個資源 —— 當 403 會洩漏「它存在」時,這也是比較有禮貌的答案
409 Conflict 版本衝突、重複、狀態機被違反
422 Unprocessable 語法沒問題但語意不合法(當你想和 400 區分開時)
429 Too Many Requests 被限流 —— 要附上 Retry-After
500 Internal Server Error 你這邊沒處理到的 bug
502 / 503 / 504 上游壞掉/服務不可用(附 Retry-After)/上游逾時

面試官特別在聽的兩個錯誤:失敗時回 200 {"error": ...}(這會弄壞每一個讀狀態列的 客戶端、代理與監控),以及把客戶端輸入錯誤回成 500。各個錯誤碼代表什麼、client 與 server 該怎麼處理 —— 要不要重試、502 和 504 差在哪 —— 見 http_errors.md。


3) 錯誤的格式 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

所有錯誤共用一種格式,而且機器先讀得懂:

json
{
  "type":     "https://api.acme.com/errors/insufficient-funds",
  "title":    "Insufficient funds",
  "status":   409,
  "detail":   "Balance 12.50 is below the required 30.00",
  "instance": "/accounts/42/withdrawals",
  "traceId":  "b7ad6b7169203331",
  "errors":   [{ "field": "amount", "message": "must be <= balance" }]
}

這就是 RFC 9457(application/problem+json),Spring 的 ProblemDetail 可以直接 產出它。不管你選哪種格式:

  • 要有穩定的錯誤代碼讓客戶端分流 —— 不要逼它們去解析人話;
  • 要有一個trace id,而且同樣出現在你的 log 裡,這樣使用者回報就變成一次查詢;
  • 驗證錯誤要附欄位層級的細節;
  • 回應裡絕對不要出現 stack trace、SQL 片段或內部主機名稱。

4) 分頁、過濾與排序 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

做法 請求 好處 壞處
Offset ?page=3&size=50 可以跳到任一頁、能顯示總數 OFFSET 100000 會變慢;資料變動時項目會在頁與頁之間跑掉
Cursor/keyset ?limit=50&cursor=eyJpZCI6MTA0Mn0 有寫入時仍然穩定;不管翻多深,成本都是一次索引定位加上該頁(O(log n + limit)) 沒有頁碼、沒有總數

動態時報、log,以及任何量大或會一直變的資料,優先用 cursor 分頁;小的後台表格用 offset 沒問題。伺服器端永遠要把 limit 設上限(客戶端要 1,000,000 筆就是一次 阻斷服務攻擊)、永遠要有預設值,並把下一個 cursor 放在回應主體或 Link header 裡。

json
{ "data": [ ... ], "nextCursor": "eyJpZCI6MTA0Mn0", "hasMore": true }

過濾與排序放在 query string(?status=open&sort=-createdAt),而且可排序的欄位要用 白名單 —— 對沒有索引的欄位排序就是全表掃描,而把欄位名字串接進 SQL 就是注入。


5) 版本管理與演進 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

做法 例子 備註
URI 路徑 /v1/orders 最醜、最清楚,對快取與 log 都友善。最常見的選擇
Header/媒體型別 Accept: application/vnd.acme.v2+json 比較「純」,但比較難測也難快取
查詢參數 ?version=2 容易加,也容易掉

更划算的技能是根本不需要新版本。向後相容的改動:加一個選填欄位、加一個端點、 加一個列舉值(前提是你有告訴客戶端要忽略不認得的值)。破壞性改動:移除或改名欄位、 把驗證變嚴、改型別或改某個值的意義、改預設值。

所以:客戶端必須忽略不認得的欄位、伺服器不能把既有欄位挪作他用,而移除要走 標記淘汰 → 公告日落時間(Deprecation/Sunset header)→ 觀察剩餘流量 → 移除。


6) 並發與快取 優先度 3/5 — 值得會 — 多半是必備模式的變形

更新遺失:兩個客戶端都讀到版本 1,都寫回去,第二個就默默把第一個蓋掉。 用樂觀並發的權杖來修:

http
GET  /orders/1042        → 200, ETag: "v7"
PUT  /orders/1042        If-Match: "v7"
                         → 200 if still v7, 412 Precondition Failed if not

值得記住的快取 header:Cache-Control: public, max-age=60(個人化的東西用 no-store)、ETag + If-None-Match → 304 Not Modified 做便宜的再驗證, 以及回應會因 header 而異時要加的 Vary(Accept-Encoding、Authorization)。


7) 安全檢查表 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

  • 只走 HTTPS,開 HSTS。憑證與 token 不要放在 URL 裡 —— URL 會落進 log、 代理與 referrer。
  • 先認證再授權,而且授權要針對每個物件,不是只看端點。 「物件層級授權失效」(GET /orders/1043 回傳別人的訂單)在實務上是 API 漏洞第一名。
  • 伺服器端驗證每一個輸入,照 schema 來 —— 長度、型別、範圍、列舉。客戶端驗證 是使用者體驗,不是安全性。
  • **絕對不要信任客戶端送來的身分、價格或角色。**價格請自己重算。
  • 針對每個主體與每個 IP 限流;被擋時回 429 並附 Retry-After。
  • 錯誤不要洩漏資訊:沒有 stack trace、登入時不要區分「查無此人」與「密碼錯誤」、 在意的話也不要用猜得到的內部 id(用 UUID/ULID)。
  • 設定請求大小上限與逾時;解析 JSON 時限制深度。
  • CORS 要明確白名單;有認證的 API 上不要出現 Access-Control-Allow-Origin: *。
  • 記錄誰做了什麼,但不要把祕密或個資內容寫進 log。

Token 的機制 —— session 與 JWT、refresh token、撤銷 —— 在 authentication.md。


8) 長時間與批次作業 優先度 3/5 — 值得會 — 多半是必備模式的變形

任何慢到不該讓請求卡著等的事,就回一個 job:

text
POST /reports            → 202 Accepted, Location: /jobs/9f2
GET  /jobs/9f2           → { "status": "running", "progress": 0.4 }
                         → { "status": "done", "result": "/reports/551" }

然後讓客戶端輪詢,或用 SSE/webhook 推給它 (web_long_connections.md)。你送出去的 webhook 也要 守你要求別人守的規矩:payload 要簽章、至少一次投遞並附事件 id 讓接收端去重, 以及帶退避的重試。接收端的做法 —— 快速 ACK、去重、DLQ、亂序事件 —— 見 webhook_integration.md。


9) REST、GraphQL 與 gRPC 優先度 3/5 — 值得會 — 多半是必備模式的變形

REST/JSON GraphQL gRPC
形狀 HTTP 上的資源 單一端點,由客戶端指定查詢 HTTP/2 + protobuf 上的型別化 RPC
取太多/取不夠 常發生 設計上就解掉了 各方法自己解
快取 HTTP 快取免費送 很難(POST、每個查詢都不同) 要自己做
Schema OpenAPI(選用) 必備,而且可內省 必備的 .proto
串流 SSE/WebSocket 外掛上去 Subscription 原生雙向
瀏覽器支援 原生 原生 需要 grpc-web
痛點 端點爆炸 查詢成本/深度限制、resolver 的 N+1 難除錯、payload 是二進位

經驗法則:對外與偏 CRUD 的 API 用 REST;在意延遲與型別契約的內部服務之間用 gRPC;當很多不同客戶端需要同一張圖的很多種投影時,用 GraphQL。


10) 面試常見問答

Q:PUT、PATCH、POST 差在哪? PUT 取代整個資源,是冪等的;PATCH 做部分更新(只有你刻意設計才是冪等的 —— 「把狀態設成 shipped」是,「數量加 1」不是);POST 建立或觸發,不是冪等的。

Q:付款 API 要怎麼做才能安全重試? 請求帶冪等鍵,並把結果和它一起存起來;同一個 key 回傳原本的回應,而不是再扣一次款。 再搭配資料庫的唯一性約束當最後一道防線。

Q:401 和 403 差在哪? 401 =我們不知道你是誰(或你的憑證過期了)。403 =我們知道你是誰,但你不可以。

Q:一個一直在被寫入的動態時報要怎麼分頁? 用不可變、單調有序的 key 做 cursor/keyset 分頁 —— 用 offset 的話,捲到一半有新資料 插進來就會重複或漏掉項目。

Q:要改一個欄位的型別,怎麼不弄壞客戶端? 在舊欄位旁邊加新欄位、兩邊都寫、讓客戶端逐步遷移、觀察用量,最後在新版本裡帶著 日落期把舊欄位拿掉。

Q:限流要放在哪裡做? 邊緣(gateway/CDN)做粗粒度的 IP 限制,服務裡做每使用者/每方案的限制,因為那需要 身分。演算法 —— token bucket、滑動視窗 —— 在 be_programming_notes.md。

Q:文件怎麼寫? 用 OpenAPI,從程式碼產生或拿它來驗證程式碼,讓兩者不會走鐘,並且每個端點、 每種錯誤都附範例。


11) 重點檢查表

text
[ ] Nouns for resources, methods for verbs; sensible nesting
[ ] Safe / idempotent / cacheable per method — and an idempotency key for POST
[ ] Correct status codes; never 200-with-error
[ ] One error shape with a stable code and a trace id
[ ] Cursor pagination, capped limits, whitelisted sort fields
[ ] Backward-compatible evolution; deprecate-then-remove
[ ] ETag + If-Match for lost updates; Cache-Control/304 for reads
[ ] Object-level authorisation, server-side validation, rate limits
[ ] 202 + job resource for long operations; signed, dedupable webhooks
[ ] When gRPC or GraphQL beats REST

參考資料