REST API 設計完全指南

深入 REST API 設計:資源與 URL、HTTP 動詞與冪等性、狀態碼、分頁、版本控制、錯誤格式與最佳實踐

本篇是 API 設計範式總覽 中 REST 的深入版;範式比較與選型看總覽篇。


目錄


什麼是 REST?

REST(Representational State Transfer) 是 Roy Fielding 在 2000 年提出的架構風格,定義了一組約束,讓系統具備可擴展、鬆耦合的特性。符合這些約束的 API 稱為 RESTful

六大約束(簡述)

約束 重點
Client-Server 前後端分離,各自演進
Stateless(無狀態) 每個請求自帶所有資訊,server 不存 session 狀態
Cacheable(可快取) 回應需標明可否快取
Uniform Interface(統一介面) 資源、HTTP 動詞、表述一致
Layered System 可分層(代理、閘道)
Code on Demand(選用) 可下發程式碼執行

實務上多數「REST API」未必完全嚴格符合,但只要遵循資源 + HTTP 動詞 + 無狀態 + 狀態碼這幾項核心,就足以稱為 RESTful。


資源與 URL 設計

REST 的核心是資源(resource):URL 是名詞(資源),動作交給 HTTP 動詞,URL 不該出現動詞

✅ 正確(名詞、複數、用動詞表達操作)
GET    /users
GET    /users/123
POST   /users
DELETE /users/123

❌ 錯誤(URL 內含動詞)
GET  /getUsers
POST /createUser
POST /users/123/delete

設計慣例

  • 用複數名詞/users 而非 /user
  • 巢狀表達從屬/users/123/orders(某使用者的訂單)
  • 巢狀別太深:超過兩層考慮拆開,如 /orders?user_id=123
  • 小寫 + 連字號/order-items 而非 /orderItems/order_items
  • 不放副檔名:用 Accept 標頭協商格式,而非 /users.json

HTTP 動詞與語意

動詞 用途 範例
GET 取得資源 GET /users/123
POST 新增資源(或非冪等操作) POST /users
PUT 整筆替換 PUT /users/123
PATCH 部分更新 PATCH /users/123
DELETE 刪除資源 DELETE /users/123

PUT vs PATCH

PUT   /users/123   { "name": "A", "email": "a@x.com" }   ← 整筆替換,未給的欄位視為清空
PATCH /users/123   { "name": "A" }                       ← 只改 name,其餘不動

安全性與冪等性

兩個常被混淆但很重要的概念:

  • 安全(Safe):不改變伺服器狀態(純讀取)
  • 冪等(Idempotent):執行一次與執行多次,結果相同
動詞 安全 冪等 說明
GET 純讀取
PUT 重複送同一份,結果一樣
DELETE 刪第二次結果仍是「不存在」
PATCH ⚠️ 看實作(set x=5 冪等;x+=1 不冪等)
POST 重複送會建立多筆

為什麼重要:冪等的請求可安全重試(網路逾時重送不會出錯)。POST 不冪等,所以表單重複送出會建立重複資料——需用 idempotency key 等機制防範。


HTTP 狀態碼

用狀態碼表達結果,別一律回 200 再把錯誤塞進 body。

常用碼

類別 意義
2xx 成功 200 OK 一般成功
201 Created 已建立(POST 成功,回 Location
204 No Content 成功但無回傳內容(常用於 DELETE)
3xx 重導 301 / 304 永久搬移 / 未修改(快取命中)
4xx 用戶端錯 400 Bad Request 請求格式/參數錯
401 Unauthorized 未認證(沒登入 / token 無效)
403 Forbidden 已認證但無權限
404 Not Found 找不到資源
409 Conflict 衝突(如唯一鍵重複)
422 Unprocessable Entity 語法對但語意驗證失敗
429 Too Many Requests 觸發限流
5xx 伺服器錯 500 Internal Server Error 伺服器例外
502 / 503 閘道錯誤 / 服務不可用

最常被混淆的是 401 vs 403:401 是「你是誰?(沒認證)」,403 是「我知道你是誰,但你不能做這件事(沒授權)」。認證/授權細節見 認證與授權


請求與回應格式(標頭與 body)

REST 走 HTTP,所以「資料長怎樣、要不要快取、用什麼格式」很大一部分由 HTTP 標頭 決定。

常用請求標頭

標頭 用途
Content-Type 本次請求 body 的格式,如 application/json
Accept 客戶端希望收到的格式(內容協商)
Authorization 認證憑證,如 Bearer <token>
If-None-Match 帶上次的 ETag,做條件式請求(命中回 304)
Idempotency-Key 客戶端產生的唯一鍵,讓 POST 可安全重試不重複建立

常用回應標頭

標頭 用途
Content-Type 本次回應 body 的格式
Location 新建資源的 URL(搭配 201)
ETag 資源版本指紋,供快取 / 條件請求
Cache-Control 快取策略,如 max-age=3600no-store
Retry-After 搭配 429 / 503,告知多久後再試

內容協商(Content Negotiation)

Accept(client 要什麼)與 Content-Type(實際給什麼)配對運作:

請求:  Accept: application/json
回應:  Content-Type: application/json; charset=utf-8

用標頭協商格式,而非在 URL 加副檔名(/users.json ❌)。談不攏時回 406 Not Acceptable

Body 格式慣例

現代 REST API 幾乎都用 JSON。常見約定:

// POST /users 的 request body
{ "name": "Alice", "email": "a@x.com" }

// 回應:201 Created
// Location: /users/123
{
  "id": 123,
  "name": "Alice",
  "email": "a@x.com",
  "createdAt": "2026-06-18T08:00:00Z"   // 時間一律用 ISO 8601 (UTC)
}
  • 欄位命名一致:整個 API 統一 camelCasesnake_case,不要混用
  • 時間用 ISO 86012026-06-18T08:00:00Z),避免各地時區歧義
  • POST 成功回傳被建立的資源 + Location 標頭指向它
  • 回傳裸物件 vs 包封套(envelope) 二選一並貫徹:
    // 裸物件(簡潔,搭 HTTP 狀態碼表達結果)
    { "id": 123, "name": "Alice" }
    
    // 封套(額外塞 meta,如分頁資訊)
    { "data": [...], "meta": { "total": 100, "nextCursor": "..." } }
    

分頁、過濾、排序

集合資源不該一次回傳全部,要支援分頁。兩種主流方式:

Offset 分頁

GET /users?limit=20&offset=40    # 第 3 頁
  • ✅ 簡單、可跳頁
  • ❌ 資料變動時會重複/遺漏;大 offset 在 DB 上很慢

Cursor 分頁(游標)

GET /users?limit=20&cursor=eyJpZCI6MTIzfQ   # 從上次最後一筆之後繼續
  • ✅ 大資料量穩定、效能好(配合索引)
  • ❌ 不能任意跳頁
  • 適合無限捲動 / 大型資料集(與 索引 的範圍查詢配合最佳)

過濾 / 排序 / 搜尋(用 query string)

GET /users?status=active&role=admin     # 過濾
GET /users?sort=-created_at,name         # 排序(- 表降冪)
GET /users?q=alice                       # 搜尋

串流與大型資料

REST 預設是「一次性回應」,但遇到大型檔案、大量資料或長時間產生的內容時,可以用 HTTP 既有機制做串流 / 分段傳輸,不必把整包載入記憶體或讓 client 空等。

1. 分塊傳輸(Chunked Transfer Encoding)

回應大到無法預先知道長度時,用 Transfer-Encoding: chunked 邊產生邊送,不需 Content-Length

HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json

伺服器框架的 streaming response(如 Node 的 stream、Python 的 generator 回應)底層就是這個。

2. 範圍請求 / 斷點續傳(Range Requests)

下載大檔可用 Range 只取一部分,支援續傳與分段下載(影音播放拖曳進度條也靠它):

請求:  Range: bytes=0-1023
回應:  206 Partial Content
        Content-Range: bytes 0-1023/50000
        Accept-Ranges: bytes

3. 串流 JSON:NDJSON / JSON Lines

回傳大量紀錄時,與其包成一個巨大的 JSON 陣列(client 得全收完才能解析),不如一行一個 JSON 物件串流,client 收一筆處理一筆:

Content-Type: application/x-ndjson

{"id":1,"name":"Alice"}
{"id":2,"name":"Bob"}
{"id":3,"name":"Carol"}

適合匯出、日誌、大型資料集;記憶體友善、可邊收邊處理。

4. 何時改用 SSE / WebSocket?

上述是「單一回應分段送達」。若需求是「server 在連線存活期間持續主動推送事件」(通知、即時更新、AI 逐字輸出),那已超出 REST 請求—回應模型,應改用 SSEWebSocket——見 API 設計範式總覽

串流回應(仍是一次請求):大檔下載、NDJSON 匯出 → chunked / Range / NDJSON
持續推送(連線上多筆事件):通知 / 即時 / AI 串流   → SSE / WebSocket

版本控制

API 演進難免破壞相容性,需要版本策略:

方式 範例 取捨
URL 路徑(最常見) /v1/users 直覺、好快取、好除錯
請求標頭 Accept: application/vnd.api.v1+json URL 乾淨,但較隱晦、不好測
查詢參數 /users?version=1 簡單但容易被忽略

多數公開 API(如 GitHub、Stripe)採 URL 路徑版本。原則:只有破壞性變更才升版;新增欄位這類向後相容的改動不需升版。


錯誤回應格式

錯誤回應要一致、可機器解析、含足夠資訊。建議參考 RFC 9457(Problem Details,前身 RFC 7807)

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
  "type": "https://example.com/errors/validation",
  "title": "Validation Failed",
  "status": 422,
  "detail": "email 格式不正確",
  "errors": [
    { "field": "email", "message": "must be a valid email" }
  ]
}

原則:

  • 正確的狀態碼(別一律 200)
  • body 提供穩定的錯誤碼 / 欄位級訊息,方便前端處理
  • 不洩漏堆疊細節 / 內部實作(安全)

Richardson 成熟度模型與 HATEOAS

衡量「有多 RESTful」的常用框架——Richardson 成熟度模型分四級:

Level 0:單一端點、用 POST 包一切(等於 RPC over HTTP)
Level 1:引入「資源」概念(多個 URL)
Level 2:正確使用 HTTP 動詞 + 狀態碼  ← 多數「REST API」在這層
Level 3:HATEOAS(回應內含可跟隨的連結)

HATEOAS

回應中附帶下一步可做什麼的連結,讓 client 不必硬編 URL:

{
  "id": 123,
  "status": "pending",
  "_links": {
    "self":   { "href": "/orders/123" },
    "cancel": { "href": "/orders/123/cancel" }
  }
}
  • ✅ 理論上最鬆耦合、可自我描述
  • ❌ 實作與客戶端成本高,實務採用率低;多數 API 停在 Level 2 已足夠

最佳實踐

  1. URL 用名詞複數、動作交給 HTTP 動詞(不要 /getUser
  2. 正確使用狀態碼,尤其分清 401(未認證)vs 403(無權限)
  3. 可安全重試的操作用冪等動詞;POST 防重複用 idempotency key
  4. 集合一定要分頁,大資料量用 cursor 分頁
  5. 破壞性變更才升版,向後相容的新增不升版
  6. 錯誤格式統一(參考 Problem Details),提供欄位級訊息
  7. 無狀態:認證資訊每次帶(如 Authorization 標頭),別依賴 server session
  8. 善用 HTTP 快取ETag / Cache-Control)— REST 相較 GraphQL 的優勢
  9. 寫 OpenAPI 規格,自動產生文件與 client

常見問題

問題 1:PUT 和 PATCH 一定要分嗎?

語意上 PUT 是整筆替換、PATCH 是部分更新。實務若只做部分更新,用 PATCH 較精確;很多 API 也只實作其一。重點是行為與動詞語意一致,別用 PUT 卻做部分更新。

問題 2:401 和 403 到底差在哪?

401 = 未認證(沒登入、token 失效);403 = 已認證但無權限(登入了但不能存取這個資源)。一句話:401 是「你是誰」,403 是「你不能」。

問題 3:刪除成功該回 200 還是 204?

都可以。204 No Content 表示成功且無回傳內容(最常見);若要回傳被刪資源或結果訊息,用 200。一致即可。

問題 4:分頁用 offset 還是 cursor?

小資料、需要跳頁 → offset 簡單夠用;大資料、無限捲動、在意效能 → cursor 分頁(避免大 offset 的全表掃描,配合索引最佳)。

問題 5:一定要做 HATEOAS 才算 REST 嗎?

理論上 HATEOAS 是 REST 的最高層(Level 3),但實務採用率低、成本高。多數 API 停在 Level 2(正確用動詞 + 狀態碼)就足以稱 RESTful 並運作良好。


總結

核心要點

  • REST = 資源(名詞 URL)+ HTTP 動詞 + 無狀態 + 狀態碼
  • URL 用複數名詞、動作交給動詞;別把動詞寫進 URL
  • 分清安全/冪等:GET/PUT/DELETE 冪等可重試,POST 不冪等
  • 狀態碼要正確,尤其 401(未認證)vs 403(無權限)
  • 集合要分頁(cursor > offset 於大資料)、破壞性變更才升版
  • 錯誤格式統一(Problem Details)、善用 HTTP 快取(ETag)
  • 多數 API 停在 Richardson Level 2 即足夠,HATEOAS 實務少見

快速參考

動詞 冪等 典型狀態碼
GET 200 / 404
POST 201 / 400 / 409
PUT 200 / 204
PATCH ⚠️ 200
DELETE 204 / 404

建立日期:2026-06-18

🔗相關文章