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=3600、no-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 統一 camelCase 或 snake_case,不要混用
  • 時間用 ISO 8601(2026-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 請求—回應模型,應改用 SSE 或 WebSocket——見 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

🔗相關文章