前一章談了 REST 與 GraphQL 的取捨。實務上前端每天都在「消費」API,理解後端設計 API 時的慣例,能讓你更快看懂文件、設計出對前端友善的介面、並正確處理各種邊界情況。本章整理幾個最常碰到的設計面向。

資源建模#

REST 的核心是「把後端能力建模成資源」,用名詞而非動詞,用 HTTP 動詞表達操作。

GET    /articles          取得文章列表
POST   /articles          建立文章
GET    /articles/42       取得單一文章
PATCH  /articles/42       部分更新
DELETE /articles/42       刪除
GET    /articles/42/comments   子資源:某篇文章的留言

URL 用名詞、操作用動詞(HTTP method)。 看到 /getArticle/createArticle?id=... 這種把動作塞進路徑的設計,通常是設計味不佳的訊號。

對於天生不是 CRUD 的操作(如「發布」「重設密碼」),常見三種慣例:

  • 把狀態當欄位:PATCH /articles/42 body { "status": "published" }
  • 建立子資源:POST /articles/42/publication
  • 務實地用動詞子路徑:POST /articles/42/publish

分頁:Offset vs Cursor#

列表 API 幾乎一定要分頁。兩種主流策略各有取捨:

策略請求形式優點缺點
Offset-based?page=3&limit=20直覺、可跳頁深分頁時 DB 慢;資料變動造成重複/遺漏
Cursor-based?limit=20&after=<cursor>效能穩定、結果一致不能任意跳頁
// Cursor 分頁:用上一頁回傳的 cursor 取下一頁
async function fetchAll(url) {
  const items = [];
  let cursor = null;
  do {
    const qs = cursor ? `?limit=50&after=${cursor}` : "?limit=50";
    const res = await fetch(url + qs);
    const { data, nextCursor } = await res.json();
    items.push(...data);
    cursor = nextCursor; // 後端回傳 null 表示沒有下一頁
  } while (cursor);
  return items;
}

Offset 分頁在「即時變動」的列表上會出問題:你看第 2 頁時若有人在第 1 頁插入一筆,整個 offset 往後位移,你會看到重複項。Cursor 以「某筆資料的位置」為基準,不受插入影響,因此社群動態、無限捲動多採 cursor。

版本控制#

一旦 API 對外,破壞性變更(移除欄位、改語意)就會打斷既有用戶端。常見版本策略:

策略範例說明
URI 路徑/v1/articles最直觀、最常見
查詢參數/articles?version=1簡單但易被忽略
自訂 HeaderX-API-Version: 1URL 乾淨,但較不透明
Accept 協商Accept: application/vnd.app.v1+json最「RESTful」,較少見

棄用舊版本要給遷移期,並用 Deprecation / Sunset 回應標頭與文件明確告知日期。GraphQL 的哲學則是「不分版本」,改用欄位層級的 @deprecated 漸進淘汰。

錯誤回應設計#

好的錯誤回應要同時對「機器」與「人」友善:用正確的 HTTP 狀態碼,並在 body 給出結構化、可操作的訊息。

{
  "type": "https://api.example.com/errors/validation",
  "title": "請求參數驗證失敗",
  "status": 422,
  "detail": "欄位 email 格式不正確",
  "errors": [{ "field": "email", "message": "must be a valid email" }]
}

上面的格式參考 RFC 7807(application/problem+json),是業界常見的錯誤格式慣例。

狀態碼含義前端該做的事
400請求格式錯誤檢查送出的 payload
401未認證導向登入 / 刷新 token
403已認證但無權限顯示「無權限」,不要重試
404資源不存在顯示空狀態
409衝突(如重複建立)提示使用者
422語意錯誤(驗證失敗)標出對應欄位
429觸發速率限制Retry-After 退避重試
5xx伺服器錯誤顯示通用錯誤、可重試

別把所有錯誤都回 200 再用 body 裡的 success: false 表示失敗。這會讓快取、監控、fetch 的錯誤處理全部失準——HTTP 狀態碼本身就是契約的一部分。

認證與授權#

方案機制適用情境
API Key固定金鑰放在 Header伺服器對伺服器、公開資料
Session Cookie伺服器存 session,瀏覽器帶 cookie傳統同源 Web App
JWT (Bearer)自帶簽章的 token,伺服器無狀態驗證SPA、行動 App、跨服務
OAuth 2.0授權框架,發放 access/refresh token第三方登入、委派授權

OAuth 2.0 的核心是「短命的 access token + 長命的 refresh token」:

sequenceDiagram
    participant C as 前端
    participant A as 授權伺服器
    participant R as 資源伺服器
    C->>A: 登入,換取 access + refresh token
    A-->>C: access token(短期)+ refresh token(長期)
    C->>R: 帶 access token 請求資源
    R-->>C: 200 資料
    Note over C,R: access token 過期
    C->>A: 用 refresh token 換新的 access token
    A-->>C: 新的 access token

Access token 設計成短命(如 15 分鐘),可大幅降低洩漏風險:就算被竊也很快失效。前端常見模式是攔截 401,自動用 refresh token 換新 token 後重送原請求。scope 則用來限定 token 能存取的範圍(如 read:articles)。

前端攔截 401 自動刷新 token 的骨架
async function apiFetch(url, options = {}) {
  let res = await fetch(url, withAuth(options));
  if (res.status === 401) {
    const ok = await refreshAccessToken(); // 用 refresh token 換新 access token
    if (ok) res = await fetch(url, withAuth(options)); // 重送一次
  }
  return res;
}

實務上還需處理「同時多個請求都收到 401」時,只刷新一次的競態控制(用一個共享的 refresh Promise)。

速率限制(Rate Limiting)#

後端為了保護資源、防止濫用,會限制單位時間內的請求數。超過時回傳 429 Too Many Requests,並透過標頭告知額度。

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1718000000
// 收到 429 時依 Retry-After 退避重試
async function fetchWithBackoff(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    const res = await fetch(url);
    if (res.status !== 429) return res;
    const wait = Number(res.headers.get("Retry-After") ?? 2 ** i);
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
  throw new Error("rate limited");
}

常見的限流演算法有固定視窗、滑動視窗與 Token Bucket,差異在於「如何分配與回補額度」;對前端而言,重點是讀懂回應標頭並做退避。

Webhook:把輪詢反過來#

當前端/客戶端需要「事件發生時被通知」,輪詢(不斷問「好了沒」)既浪費又有延遲。Webhook 反過來,由伺服器在事件發生時主動 POST 到你註冊的網址。

接收 Webhook 的關鍵實務:

  • 驗章:用共享密鑰對 payload 做 HMAC,比對簽章標頭,確認來源可信。
  • 冪等:同一事件可能被重送,用事件 ID 去重,避免重複處理。
  • 快速回 200:先回應再非同步處理,避免對方因逾時而重試。

小結#

面向重點
資源URL 用名詞、操作用 HTTP 動詞
分頁變動列表用 cursor,靜態可跳頁用 offset
版本URI 版本最常見;棄用要給遷移期
錯誤正確狀態碼 + 結構化 body(RFC 7807)
認證短命 access token + refresh token;前端攔 401 自動刷新
限流Retry-After 做退避重試
Webhook驗章、冪等、快速回應