前一章談了 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/42body{ "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 | 簡單但易被忽略 |
| 自訂 Header | X-API-Version: 1 | URL 乾淨,但較不透明 |
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 tokenAccess 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 | 驗章、冪等、快速回應 |