RESTful API 設計全攻略:從資源命名到 HATEOAS,一次搞懂最佳實踐

最後更新:2025年3月4日
架構

為什麼每個人都說要「RESTful」?

當你學習 Web 開發時,一定會不斷聽到「RESTful API」這個詞。

它聽起來像是某種神聖不可侵犯的規範,但其實它的核心只是一組讓 API 更好用、更直覺的設計原則

本文將揭開 RESTful API 的神秘面紗,從「資源命名」到「狀態碼選擇」,搭配大量實際案例,帶你掌握設計優雅 API 的關鍵心法。


資源導向設計:把一切當作「名詞」

在設計 API 時,一個核心概念是資源導向設計(Resource-Oriented Design,ROD)。

這種方法強調 API 的結構應該圍繞資源來構建,而不是基於動作(動詞)。

換句話說,API 應該回答「我們在操作什麼?」而不是「我們在做什麼?」

什麼是資源(Resource)?

在 RESTful API 設計中,資源是 API 操作的核心對象,任何可以被唯一標識、管理的事物都可以視為資源。

資源的範圍可以涵蓋具體的實體、抽象概念,甚至是某種集合。

常見的資源類型

  1. 實體物件(Entity):指的是具體存在的物品或用戶,例如:
    • 商品(Product)
    • 用戶(User)
  2. 抽象概念(Concept):指的是非具體的業務概念,例如:
    • 訂單(Order)
    • 權限(Permission)
  3. 集合(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 來獲取資源,而不是 POSTDELETE 來達成相同目的。

五大核心 HTTP 方法與對應語意

操作意圖取得資源
對應 SQLSELECT
是否冪等(Idempotent)✅ 是
操作意圖新增資源
對應 SQLINSERT
是否冪等(Idempotent)❌ 否
操作意圖替換整個資源
對應 SQLUPDATE
是否冪等(Idempotent)✅ 是
操作意圖部分更新資源
對應 SQLUPDATE
是否冪等(Idempotent)❌ 否
操作意圖刪除資源
對應 SQLDELETE
是否冪等(Idempotent)✅ 是

🔹 冪等性解釋

冪等(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 狀態碼由三位數字組成,第一個數字表示該狀態碼的類別。狀態碼可大致分為五大類,每一類的作用如下:

類別資訊性回應
常見範例100 Continue
說明客戶端可繼續請求(較少使用)
類別成功
常見範例200 OK, 201 Created
說明表示請求成功
類別重定向
常見範例301 Moved Permanently, 302 Found
說明需要進一步操作,例如重新導向
類別客戶端錯誤
常見範例400 Bad Request, 404 Not Found
說明客戶端請求錯誤
類別伺服器錯誤
常見範例500 Internal Server Error
說明伺服器發生錯誤

通常,在 API 設計中,2xx、4xx 和 5xx 類別的狀態碼是最常用的,因為它們分別表示成功、客戶端錯誤和伺服器錯誤的不同情境。

精選 HTTP 狀態碼使用情境

不同的 HTTP 狀態碼對應不同的 API 操作,以下是最常見的狀態碼及其適用場景:

🔹 成功類(2xx)

名稱請求成功
使用時機GET 成功取得資源
回應範例(JSON){ "data": [...] }
名稱資源已建立
使用時機POST 成功新增資源
回應範例(JSON){ "id": 123, "name": "New Product" }
名稱請求成功但無內容
使用時機DELETE 操作成功,無需回傳內容
回應範例(JSON)無回應內容

200 OK 是最常見的狀態碼,適用於讀取GET)操作。

201 Created 則適用於成功新增資源POST),並且通常會在回應中返回新資源的 ID 或完整內容

204 No Content 則用於成功處理但無需回應內容的情境,例如 DELETE 操作。

🔹 客戶端錯誤類(4xx)

名稱請求格式錯誤
使用時機傳入的參數格式錯誤、不合法
回應範例(JSON){ "error": "Invalid email format" }
名稱未授權
使用時機缺少或提供了無效的身份驗證 Token
回應範例(JSON){ "error": "Missing authentication token" }
名稱禁止訪問
使用時機Token 有效,但權限不足
回應範例(JSON){ "error": "You do not have permission to access this resource" }
名稱資源不存在
使用時機查詢的資源 ID 不存在
回應範例(JSON){ "error": "Product 123 not found" }
名稱請求過多
使用時機客戶端請求過於頻繁,觸發速率限制
回應範例(JSON){ "error": "Rate limit exceeded" }

這些狀態碼通常用來幫助 API 告知客戶端發生了什麼錯誤

例如,400 代表請求格式錯誤,404 則表示資源不存在,而 401403 則涉及身份驗證和授權問題。

🔹 伺服器錯誤類(5xx)

名稱內部伺服器錯誤
使用時機伺服器發生未預期錯誤
回應範例(JSON){ "error": "An unexpected error occurred" }
名稱服務不可用
使用時機伺服器過載或維護中
回應範例(JSON){ "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"
}

這樣的做法有以下優勢:

  1. 更直觀:客戶端可以直接透過狀態碼判斷請求是否成功,而不需要解析 JSON 內容。
  2. 減少額外邏輯:前端或其他 API 使用者不需要額外撰寫判斷 status 欄位的邏輯,只需檢查 HTTP 狀態碼即可。
  3. 符合標準:這是 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 架構中的一個重要特性,核心思想可以歸納為以下幾點:

  1. API 回應中提供相關操作的超連結:每個 API 回應不僅返回資源的數據,還包含與該資源相關的操作連結,例如查看詳細資訊、編輯、刪除等。
  2. 客戶端不需預先知道所有 API 端點:與傳統 API 不同,客戶端可以透過 API 回應內的 _links 欄位來動態發現可用的操作,而不必硬編碼(Hardcoded)所有 API URL。
  3. 類似網頁的「超連結導航」體驗: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 }
    }
  }
}

🔹 解釋這段回應:

  1. self:表示當前資源的 API 端點,即 /products/123,客戶端可用來重新取得該商品資訊。
  2. reviews:提供該商品的評論 API,客戶端可以使用這個連結來獲取商品評論。
  3. 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/reviews URL,而是從 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 回應中需要包含額外的 _links 資訊,可能會使請求的回應大小增加,影響傳輸效率。
挑戰需設計統一的連結格式標準:不同 API 可能會有不同的 _links 格式,團隊需要制定一致的標準來確保 API 的可讀性與一致性。
挑戰對新手來說概念較抽象:與傳統 API 相比,HATEOAS 需要更深的理解,開發者需要習慣透過 API 回應中的連結來操作資源,而不是直接調用固定的 API URL。

HATEOAS 適用場景與實務應用

HATEOAS 在以下幾種情境中特別有用:

  1. API 變動頻繁的系統
    • 如果 API 經常新增或修改端點,HATEOAS 能讓客戶端適應變更,而不需要頻繁更新程式碼。
  2. 複雜的業務流程
    • 例如電商平台中的訂單狀態變更,當訂單處於「待付款」、「已付款」、「已出貨」等不同狀態時,HATEOAS 可動態提供相應的操作(例如「付款」、「取消訂單」、「查看物流」等)。
  3. 需要提升 API 可探索性的場景
    • 當開發團隊希望 API 更加自描述,使開發者能夠透過 API 回應了解可用操作時,HATEOAS 是一個很好的選擇。

🔹 但 HATEOAS 不適用於:

  • 簡單的 API:如果 API 端點固定且變動不大,HATEOAS 可能會增加不必要的複雜性。
  • 對效能要求極高的場景:由於 HATEOAS 會增加回應的大小,可能會影響 API 的性能,因此在高併發場景下需要謹慎考量。

總結:RESTful 不是教條,而是溝通默契

RESTful API 的四大原則,最終目標是建立開發者之間的共同語言

  1. 資源導向設計 → 讓 URI 直觀如文件路徑
  2. HTTP 方法語意化 → 像使用資料庫一樣操作資源
  3. 狀態碼規範 → 用數字快速傳遞結果
  4. HATEOAS → 讓 API 自己告訴你能做什麼

這些原則並非鐵律,實際開發中可根據需求調整。

關鍵在於保持一致性,讓團隊成員與第三方開發者都能輕鬆理解你的設計。

新手常見問答

一定要用複數名詞嗎?例如 /product 可以嗎?

複數是為了統一性(如 /products/123/product/123 更直覺),但非強制。重點是團隊保持一致。

PUT 和 PATCH 到底差在哪?

PUT 替換整個資源(需傳遞完整欄位),PATCH 只更新部分欄位(傳遞差異即可)。

HATEOAS 實務上真的有用嗎?

在大型系統或需要高度可探索性的 API(如公共服務)中價值明顯,但小型專案可先省略。