本篇是 API 設計範式總覽 中 REST 的深入版;範式比較與選型看總覽篇。
目錄
- 什麼是 REST?
- 資源與 URL 設計
- HTTP 動詞與語意
- 安全性與冪等性
- HTTP 狀態碼
- 請求與回應格式(標頭與 body)
- 分頁、過濾、排序
- 串流與大型資料
- 版本控制
- 錯誤回應格式
- Richardson 成熟度模型與 HATEOAS
- 最佳實踐
- 常見問題
- 總結
什麼是 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 已足夠
最佳實踐
- URL 用名詞複數、動作交給 HTTP 動詞(不要
/getUser) - 正確使用狀態碼,尤其分清 401(未認證)vs 403(無權限)
- 可安全重試的操作用冪等動詞;POST 防重複用 idempotency key
- 集合一定要分頁,大資料量用 cursor 分頁
- 破壞性變更才升版,向後相容的新增不升版
- 錯誤格式統一(參考 Problem Details),提供欄位級訊息
- 無狀態:認證資訊每次帶(如
Authorization標頭),別依賴 server session - 善用 HTTP 快取(
ETag/Cache-Control)— REST 相較 GraphQL 的優勢 - 寫 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