RESTful API 設計全攻略:從資源命名到 HATEOAS,一次搞懂最佳實踐
更新日期: 2025 年 3 月 4 日
本文為 Web 架構進化史 系列文,第 4 篇:
為什麼每個人都說要「RESTful」?
當你學習 Web 開發時,一定會不斷聽到「RESTful API」這個詞。
它聽起來像是某種神聖不可侵犯的規範,但其實它的核心只是一組讓 API 更好用、更直覺的設計原則。
本文將揭開 RESTful API 的神秘面紗,從「資源命名」到「狀態碼選擇」,搭配大量實際案例,帶你掌握設計優雅 API 的關鍵心法。
資源導向設計:把一切當作「名詞」
在設計 API 時,一個核心概念是資源導向設計(Resource-Oriented Design,ROD)。
這種方法強調 API 的結構應該圍繞資源來構建,而不是基於動作(動詞)。
換句話說,API 應該回答「我們在操作什麼?」而不是「我們在做什麼?」。
什麼是資源(Resource)?
在 RESTful API 設計中,資源是 API 操作的核心對象,任何可以被唯一標識、管理的事物都可以視為資源。
資源的範圍可以涵蓋具體的實體、抽象概念,甚至是某種集合。
常見的資源類型
- 實體物件(Entity):指的是具體存在的物品或用戶,例如:
- 商品(Product)
- 用戶(User)
- 抽象概念(Concept):指的是非具體的業務概念,例如:
- 訂單(Order)
- 權限(Permission)
- 集合(Collection):指的是某類資源的集合,例如:
- 所有商品
/products - 熱門文章列表
/articles/popular
- 所有商品
這些資源應該通過 API 提供 CRUD(Create、Read、Update、Delete)操作,並且遵循統一的設計規範來提升可讀性與可維護性。
設計資源 URI 的黃金法則
在 RESTful API 設計中,URI(統一資源識別符,Uniform Resource Identifier)應該清楚地表達資源的概念,而不是操作的動作。
以下是幾個關鍵原則:
🔹 1. 用名詞,不用動詞
URI 應該描述「是什麼」,而不是「做什麼」。
🚫 錯誤設計(帶有動詞):
GET /getProducts
POST /createUser
PUT /updateProduct/123
✅ 正確設計(只使用名詞):
GET /products
POST /users
PUT /products/123
🔹 2. 使用複數形式
統一使用複數名詞來表示資源集合,以保持一致性。例如:
✅ /products (所有商品)
✅ /users (所有用戶)
這樣能讓開發者直覺地理解這個 URI 代表的是一組資源,而不是單個資源。
🔹 3. 使用層次結構(階層式結構)
用 / 來表達資源之間的從屬關係。
例如:
/products/123/reviews代表商品 123 的所有評論/users/456/orders代表用戶 456 的所有訂單
這種層次結構讓 API 更加直觀,並且符合資源的自然關係。
🚫 錯誤設計:
GET /getReviewsByProductId?productId=123
✅ 正確設計:
GET /products/123/reviews
實際案例:電商平台 API 設計
以下是一個典型的電商平台 API 設計,示範如何用 RESTful 的方式來設計資源及其操作。
🔹 商品相關操作
GET /products # 取得所有商品
POST /products # 新增商品
GET /products/123 # 取得商品 123
PUT /products/123 # 更新商品 123
DELETE /products/123 # 刪除商品 123
🔹 商品評論(從屬資源)
GET /products/123/reviews # 取得商品 123 的所有評論
POST /products/123/reviews # 新增評論到商品 123
這樣的設計方式符合層次結構,讓 API 易讀且具備可擴展性。
HTTP 方法語意化:動詞的標準化
RESTful API 的精髓之一是充分利用 HTTP 方法來表達操作意圖,而不是透過自訂動作來混淆語意。
例如,應該使用 GET 來獲取資源,而不是 POST 或 DELETE 來達成相同目的。
五大核心 HTTP 方法與對應語意
| HTTP 方法 | 操作意圖 | 對應 SQL | 是否冪等(Idempotent) |
|---|---|---|---|
| GET | 取得資源 | SELECT | ✅ 是 |
| POST | 新增資源 | INSERT | ❌ 否 |
| PUT | 替換整個資源 | UPDATE | ✅ 是 |
| PATCH | 部分更新資源 | UPDATE | ❌ 否 |
| DELETE | 刪除資源 | DELETE | ✅ 是 |
🔹 冪等性解釋
冪等(Idempotency) 指的是重複執行相同操作時,結果應該保持一致。例如:
- DELETE
/products/123不管執行多少次,結果都是資源被刪除(或已不存在),所以是冪等的。 - POST
/products每次執行都會產生新的商品,因此不是冪等的。
常見錯誤與修正案例
🚫 錯誤範例:用 GET 來執行刪除操作
GET /deleteProduct?id=123 # ❌ 違反 RESTful 規範,GET 應該只用來讀取數據✅ 正確做法:使用 DELETE 方法
DELETE /products/123 # ✅ 明確表達刪除意圖這樣的做法不僅符合 HTTP 標準,也能讓 API 更加一致且具備可預測性,減少開發和維護成本。
狀態碼規範:用數字說故事的藝術
在 RESTful API 設計中,HTTP 狀態碼扮演著 API 與客戶端溝通的關鍵角色。
它就像 API 的「表情符號」,讓客戶端能夠透過數字直覺地理解請求的結果。
選擇適當的狀態碼不僅能提高 API 的可讀性,還能幫助開發者快速診斷錯誤並採取適當的處理措施。
必知的五大類 HTTP 狀態碼
HTTP 狀態碼由三位數字組成,第一個數字表示該狀態碼的類別。狀態碼可大致分為五大類,每一類的作用如下:
| 狀態碼範圍 | 類別 | 常見範例 | 說明 |
|---|---|---|---|
| 1xx | 資訊性回應 | 100 Continue | 客戶端可繼續請求(較少使用) |
| 2xx | 成功 | 200 OK, 201 Created | 表示請求成功 |
| 3xx | 重定向 | 301 Moved Permanently, 302 Found | 需要進一步操作,例如重新導向 |
| 4xx | 客戶端錯誤 | 400 Bad Request, 404 Not Found | 客戶端請求錯誤 |
| 5xx | 伺服器錯誤 | 500 Internal Server Error | 伺服器發生錯誤 |
通常,在 API 設計中,2xx、4xx 和 5xx 類別的狀態碼是最常用的,因為它們分別表示成功、客戶端錯誤和伺服器錯誤的不同情境。
精選 HTTP 狀態碼使用情境
不同的 HTTP 狀態碼對應不同的 API 操作,以下是最常見的狀態碼及其適用場景:
🔹 成功類(2xx)
| 狀態碼 | 名稱 | 使用時機 | 回應範例(JSON) |
|---|---|---|---|
| 200 OK | 請求成功 | GET 成功取得資源 | { "data": [...] } |
| 201 Created | 資源已建立 | POST 成功新增資源 | { "id": 123, "name": "New Product" } |
| 204 No Content | 請求成功但無內容 | DELETE 操作成功,無需回傳內容 | 無回應內容 |
200 OK 是最常見的狀態碼,適用於讀取(GET)操作。
201 Created 則適用於成功新增資源(POST),並且通常會在回應中返回新資源的 ID 或完整內容。
204 No Content 則用於成功處理但無需回應內容的情境,例如 DELETE 操作。
🔹 客戶端錯誤類(4xx)
| 狀態碼 | 名稱 | 使用時機 | 回應範例(JSON) |
|---|---|---|---|
| 400 Bad Request | 請求格式錯誤 | 傳入的參數格式錯誤、不合法 | { "error": "Invalid email format" } |
| 401 Unauthorized | 未授權 | 缺少或提供了無效的身份驗證 Token | { "error": "Missing authentication token" } |
| 403 Forbidden | 禁止訪問 | Token 有效,但權限不足 | { "error": "You do not have permission to access this resource" } |
| 404 Not Found | 資源不存在 | 查詢的資源 ID 不存在 | { "error": "Product 123 not found" } |
| 429 Too Many Requests | 請求過多 | 客戶端請求過於頻繁,觸發速率限制 | { "error": "Rate limit exceeded" } |
這些狀態碼通常用來幫助 API 告知客戶端發生了什麼錯誤。
例如,400 代表請求格式錯誤,404 則表示資源不存在,而 401 和 403 則涉及身份驗證和授權問題。
🔹 伺服器錯誤類(5xx)
| 狀態碼 | 名稱 | 使用時機 | 回應範例(JSON) |
|---|---|---|---|
| 500 Internal Server Error | 內部伺服器錯誤 | 伺服器發生未預期錯誤 | { "error": "An unexpected error occurred" } |
| 503 Service Unavailable | 服務不可用 | 伺服器過載或維護中 | { "error": "Server is under maintenance" } |
這些錯誤通常來自於伺服器端的問題,而非客戶端的請求錯誤。
新手陷阱:濫用 200 OK 狀態碼來包裹錯誤
🚫 錯誤示範:使用 200 OK 回應錯誤
{
"status": "error",
"message": "Product not found"
}
這種做法雖然讓所有回應都使用 200 OK,但卻違反了 RESTful API 的原則,因為客戶端無法透過狀態碼區分成功與失敗,必須額外解析 JSON 內容來判斷錯誤類型,這會增加開發和維護的複雜度。
✅ 正確做法:使用適當的 HTTP 狀態碼
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": "Product 123 not found"
}
這樣的做法有以下優勢:
- 更直觀:客戶端可以直接透過狀態碼判斷請求是否成功,而不需要解析 JSON 內容。
- 減少額外邏輯:前端或其他 API 使用者不需要額外撰寫判斷
status欄位的邏輯,只需檢查 HTTP 狀態碼即可。 - 符合標準:這是 RESTful API 設計的最佳實踐。
這裡是更詳細且完整的版本,擴充了 HATEOAS 的概念、優勢與挑戰,並提供更具體的案例和應用情境。
HATEOAS:讓 API 會「自我介紹」
在 RESTful API 設計中,HATEOAS(Hypermedia As The Engine Of Application State)是一個強調 API 自描述性的重要概念。
傳統 API 設計通常要求客戶端事先知道所有 API 端點(Endpoint),但 HATEOAS 提供了一種更靈活的方法,使 API 具備「超連結導航」的能力。
讓客戶端能夠透過 API 回應內的連結,來動態發現可執行的操作。
這種設計方式讓 API 更具可探索性,減少客戶端與伺服器的耦合,使系統更易於擴展和維護。
什麼是 HATEOAS?
HATEOAS 是 REST 架構中的一個重要特性,核心思想可以歸納為以下幾點:
- API 回應中提供相關操作的超連結:每個 API 回應不僅返回資源的數據,還包含與該資源相關的操作連結,例如查看詳細資訊、編輯、刪除等。
- 客戶端不需預先知道所有 API 端點:與傳統 API 不同,客戶端可以透過 API 回應內的
_links欄位來動態發現可用的操作,而不必硬編碼(Hardcoded)所有 API URL。 - 類似網頁的「超連結導航」體驗:HATEOAS 提供的超連結就像網頁中的超連結,使用者可以點擊超連結來瀏覽不同的頁面,而客戶端則可以透過
_links來發現並調用 API。
這樣的設計方式使得 API 更具適應性,即使 API 結構發生變化,客戶端仍然能夠透過 API 回應內的連結來動態適應,而不需要手動修改程式碼。
實際案例:帶有超連結的產品資料
假設我們有一個電商平台的 API,當客戶端請求獲取某個商品的詳細資訊時,HATEOAS 方式的 API 回應可能如下:
{
"id": 123,
"name": "無線耳機",
"price": 2990,
"_links": {
"self": { "href": "/products/123" },
"reviews": { "href": "/products/123/reviews" },
"addToCart": {
"href": "/cart/items",
"method": "POST",
"body": { "productId": 123 }
}
}
}
🔹 解釋這段回應:
self:表示當前資源的 API 端點,即/products/123,客戶端可用來重新取得該商品資訊。reviews:提供該商品的評論 API,客戶端可以使用這個連結來獲取商品評論。addToCart:提供加入購物車的 API,並包含請求方法(POST)與請求參數(body),客戶端可以直接根據這個資訊發送請求,而無需事先知道 API 規則。
🔹 客戶端如何應用?
當前端應用收到這個 API 回應時,它可以動態讀取 _links,決定要顯示哪些按鈕與如何發送請求,而不需要手動寫死 API URL。
1️⃣ 顯示「查看評論」按鈕
如果 _links 中包含 reviews,表示這個商品有評論功能,前端就可以顯示「查看評論」按鈕,並動態取得評論 API 的 URL:
<button id="reviewButton" style="display: none">查看評論</button>// 取得商品資訊,並根據 _links 動態生成按鈕
fetch("/products/123")
.then(response => response.json())
.then(product => {
if (product._links.reviews) {
const reviewButton = document.getElementById("reviewButton");
reviewButton.style.display = "block"; // 顯示按鈕
reviewButton.onclick = () => fetchReviews(product._links.reviews.href);
}
});
function fetchReviews(url) {
fetch(url)
.then(response => response.json())
.then(data => console.log("商品評論:", data));
}
✅ 這樣做的好處:
- 不會寫死
/products/123/reviewsURL,而是從 API 回應中獲取。 - 如果 API 端點變更(例如變成
/items/123/comments),客戶端仍然能正常運作,無需修改代碼。 - API 回應內沒有
reviews時,不會顯示按鈕,確保 UI 只顯示可執行的操作。
2️⃣ 顯示「加入購物車」按鈕
如果 _links 中包含 addToCart,表示這個商品可以加入購物車,前端可以顯示「加入購物車」按鈕,並根據 API 回應發送請求:
<button id="cartButton" style="display: none">加入購物車</button>// 取得商品資訊,並根據 _links 動態生成按鈕
fetch("/products/123")
.then(response => response.json())
.then(product => {
if (product._links.addToCart) {
const cartButton = document.getElementById("cartButton");
cartButton.style.display = "block"; // 顯示按鈕
cartButton.onclick = () => addToCart(product._links.addToCart);
}
});
function addToCart(cartLink) {
fetch(cartLink.href, {
method: cartLink.method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(cartLink.body)
})
.then(response => response.json())
.then(data => console.log("加入購物車結果:", data));
}
✅ 這樣做的好處:
POST目標 URL 並未寫死,而是從 API 回應的_links內動態獲取。POST所需的參數(body)也來自 API 回應,確保前端無需手動定義請求格式,減少錯誤。- 如果未來購物車 API 變更(例如
/cart/items變成/shopping-cart/add),客戶端依然能正常運作,無需修改代碼。
為什麼這樣做更靈活?
✅ 避免硬編碼 API URL:
- 如果 API 變更,客戶端無需修改代碼,只需依據 API 回應的
_links發送請求。
✅ 根據 API 回應來決定顯示哪些功能:
- API 未提供
reviews時,前端不會顯示「查看評論」按鈕,避免無效操作。
✅ 提升 API 的可探索性:
_links讓開發者能夠透過 API 回應自動發現可用操作,而不需要查閱 API 文件。
HATEOAS 的優缺點
雖然 HATEOAS 為 API 設計帶來了許多優勢,但也有一些挑戰需要克服。
以下是 HATEOAS 的優勢與可能的挑戰:
| 優勢 | 挑戰 |
|---|---|
| 降低客戶端與伺服器的耦合度:API 回應提供所有操作的超連結,客戶端不需手動設定 API URL,當 API 更新時,客戶端仍能適應變更。 | 增加回應內容大小:因為 API 回應中需要包含額外的 _links 資訊,可能會使請求的回應大小增加,影響傳輸效率。 |
| 動態適應 API 變化:當 API 端點變更時,只要回應的 _links 保持更新,客戶端仍然可以正常運行,無需修改代碼。 | 需設計統一的連結格式標準:不同 API 可能會有不同的 _links 格式,團隊需要制定一致的標準來確保 API 的可讀性與一致性。 |
| 提升 API 可探索性:HATEOAS 讓 API 變得更直觀,開發者可以透過 API 回應內的超連結來理解可用的操作,而不需要查閱 API 文件。 | 對新手來說概念較抽象:與傳統 API 相比,HATEOAS 需要更深的理解,開發者需要習慣透過 API 回應中的連結來操作資源,而不是直接調用固定的 API URL。 |
HATEOAS 適用場景與實務應用
HATEOAS 在以下幾種情境中特別有用:
- API 變動頻繁的系統
- 如果 API 經常新增或修改端點,HATEOAS 能讓客戶端適應變更,而不需要頻繁更新程式碼。
- 複雜的業務流程
- 例如電商平台中的訂單狀態變更,當訂單處於「待付款」、「已付款」、「已出貨」等不同狀態時,HATEOAS 可動態提供相應的操作(例如「付款」、「取消訂單」、「查看物流」等)。
- 需要提升 API 可探索性的場景
- 當開發團隊希望 API 更加自描述,使開發者能夠透過 API 回應了解可用操作時,HATEOAS 是一個很好的選擇。
🔹 但 HATEOAS 不適用於:
- 簡單的 API:如果 API 端點固定且變動不大,HATEOAS 可能會增加不必要的複雜性。
- 對效能要求極高的場景:由於 HATEOAS 會增加回應的大小,可能會影響 API 的性能,因此在高併發場景下需要謹慎考量。
總結:RESTful 不是教條,而是溝通默契
RESTful API 的四大原則,最終目標是建立開發者之間的共同語言:
- 資源導向設計 → 讓 URI 直觀如文件路徑
- HTTP 方法語意化 → 像使用資料庫一樣操作資源
- 狀態碼規範 → 用數字快速傳遞結果
- HATEOAS → 讓 API 自己告訴你能做什麼
這些原則並非鐵律,實際開發中可根據需求調整。
關鍵在於保持一致性,讓團隊成員與第三方開發者都能輕鬆理解你的設計。
新手常見問答
複數是為了統一性(如 /products/123 比 /product/123 更直覺),但非強制。重點是團隊保持一致。
PUT 替換整個資源(需傳遞完整欄位),PATCH 只更新部分欄位(傳遞差異即可)。
在大型系統或需要高度可探索性的 API(如公共服務)中價值明顯,但小型專案可先省略。
