跳至主要內容
Skip to content

RESTful API 設計原則:資源導向的藝術

REST(Representational State Transfer,表現層狀態轉換)是一種軟體架構風格。本篇將介紹如何設計優雅、一致的 RESTful API。


一、 REST 核心概念

1.1 六大約束

約束說明
Client-Server客戶端與伺服器分離
Stateless每個請求獨立,不依賴狀態
Cacheable回應必須標明是否可快取
Uniform Interface統一介面
Layered System分層架構
Code on Demand可選,伺服器可傳送可執行程式碼

1.2 資源導向思維


二、 URL 設計

2.1 命名規範

bash
# ✅ 資源用名詞複數
GET /users
GET /products
GET /orders

# ❌ 避免動詞
GET /getUsers
GET /fetchProducts

# ✅ 使用 kebab-case
GET /user-profiles
GET /order-items

# ❌ 避免其他格式
GET /userProfiles
GET /user_profiles

2.2 資源層級

bash
# 集合
GET /users              # 所有用戶
POST /users             # 創建用戶

# 單一資源
GET /users/123          # 單一用戶
PUT /users/123          # 更新用戶
DELETE /users/123       # 刪除用戶

# 子資源
GET /users/123/orders           # 用戶的訂單
GET /users/123/orders/456       # 特定訂單
POST /users/123/orders          # 為用戶創建訂單

2.3 查詢參數

bash
# 過濾
GET /products?category=electronics&brand=apple

# 排序
GET /products?sort=price&order=desc

# 分頁
GET /products?page=2&limit=20

# 欄位選擇
GET /users?fields=id,name,email

# 搜尋
GET /products?q=iphone

# 組合
GET /products?category=electronics&sort=-price&page=1&limit=10

三、 HTTP 方法對應

3.1 CRUD 操作

操作HTTP 方法URL說明
CreatePOST/users創建資源
ReadGET/users, /users/1讀取資源
UpdatePUT/users/1完整更新
UpdatePATCH/users/1部分更新
DeleteDELETE/users/1刪除資源

3.2 PUT vs PATCH

javascript
// PUT:完整替換
PUT /users/1
{
  "name": "John",
  "email": "john@example.com",
  "age": 30,
  "address": "..."
}

// PATCH:部分更新
PATCH /users/1
{
  "email": "newemail@example.com"
}

3.3 非 CRUD 操作

bash
# 方法 1:子資源
POST /orders/123/cancel
POST /users/123/activate

# 方法 2:動作資源
POST /password-reset
POST /email-verification

# 方法 3:查詢參數
PATCH /orders/123?action=cancel

四、 回應設計

4.1 狀態碼

javascript
// 成功
200 OK          // GET、PUT、PATCH 成功
201 Created     // POST 創建成功
204 No Content  // DELETE 成功

// 重導向
301 Moved Permanently
304 Not Modified

// 客戶端錯誤
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
422 Unprocessable Entity

// 伺服器錯誤
500 Internal Server Error
503 Service Unavailable

4.2 回應格式

javascript
// 單一資源
GET /users/1
{
  "id": 1,
  "name": "John Doe",
  "email": "john@example.com",
  "createdAt": "2025-01-13T00:00:00Z"
}

// 集合資源
GET /users
{
  "data": [
    { "id": 1, "name": "John" },
    { "id": 2, "name": "Jane" }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 100,
    "totalPages": 5
  }
}

// 創建成功
POST /users
// 201 Created
// Location: /users/123
{
  "id": 123,
  "name": "New User"
}

4.3 錯誤格式

javascript
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" },
      { "field": "name", "message": "Name is required" }
    ]
  }
}

五、 進階設計

5.1 HATEOAS

HATEOAS(Hypermedia as the Engine of Application State)讓 API 自我描述:

javascript
GET /users/1
{
  "id": 1,
  "name": "John",
  "links": [
    { "rel": "self", "href": "/users/1" },
    { "rel": "orders", "href": "/users/1/orders" },
    { "rel": "profile", "href": "/users/1/profile" },
    { "rel": "update", "href": "/users/1", "method": "PUT" },
    { "rel": "delete", "href": "/users/1", "method": "DELETE" }
  ]
}

5.2 資源展開

bash
# 預設:只返回 ID
GET /orders/1
{
  "id": 1,
  "userId": 123,
  "productIds": [10, 20]
}

# 展開關聯資源
GET /orders/1?expand=user,products
{
  "id": 1,
  "user": { "id": 123, "name": "John" },
  "products": [
    { "id": 10, "name": "Product A" },
    { "id": 20, "name": "Product B" }
  ]
}

5.3 批次操作

javascript
// 批次獲取
GET /users?ids=1,2,3

// 批次創建
POST /users/batch
{
  "users": [
    { "name": "User 1" },
    { "name": "User 2" }
  ]
}

// 批次更新
PATCH /users/batch
{
  "updates": [
    { "id": 1, "status": "active" },
    { "id": 2, "status": "inactive" }
  ]
}

// 批次刪除
DELETE /users?ids=1,2,3

六、 最佳實踐

6.1 一致性

javascript
// ✅ 所有端點使用相同格式
{
  "data": ...,
  "error": ...,
  "pagination": ...
}

// ✅ 統一的命名風格
camelCase for JSON
kebab-case for URLs

// ✅ 統一的日期格式
ISO 8601: "2025-01-13T00:00:00Z"

6.2 安全

javascript
// 認證
Authorization: Bearer <token>

// 限流
RateLimit-Limit: 100
RateLimit-Remaining: 95
RateLimit-Reset: 1640000000

// 驗證輸入
app.post('/users', validateBody(userSchema), createUser)

6.3 文檔

yaml
# OpenAPI 規範
openapi: 3.0.0
paths:
  /users:
    get:
      summary: List all users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
      responses:
        200:
          description: Success

七、 Express 實作

javascript
const express = require("express");
const router = express.Router();

// GET /users
router.get("/", async (req, res) => {
  const { page = 1, limit = 20, sort, order } = req.query;

  const users = await User.find()
    .sort({ [sort]: order === "desc" ? -1 : 1 })
    .skip((page - 1) * limit)
    .limit(limit);

  const total = await User.countDocuments();

  res.json({
    data: users,
    pagination: {
      page: Number(page),
      limit: Number(limit),
      total,
      totalPages: Math.ceil(total / limit),
    },
  });
});

// GET /users/:id
router.get("/:id", async (req, res) => {
  const user = await User.findById(req.params.id);

  if (!user) {
    return res.status(404).json({
      error: { code: "NOT_FOUND", message: "User not found" },
    });
  }

  res.json(user);
});

// POST /users
router.post("/", validateBody(createUserSchema), async (req, res) => {
  const user = await User.create(req.body);

  res.status(201).location(`/users/${user.id}`).json(user);
});

// PUT /users/:id
router.put("/:id", validateBody(updateUserSchema), async (req, res) => {
  const user = await User.findByIdAndUpdate(req.params.id, req.body, {
    new: true,
    runValidators: true,
  });

  if (!user) {
    return res.status(404).json({
      error: { code: "NOT_FOUND", message: "User not found" },
    });
  }

  res.json(user);
});

// DELETE /users/:id
router.delete("/:id", async (req, res) => {
  const user = await User.findByIdAndDelete(req.params.id);

  if (!user) {
    return res.status(404).json({
      error: { code: "NOT_FOUND", message: "User not found" },
    });
  }

  res.status(204).end();
});

module.exports = router;

總結

原則說明
資源導向用名詞不用動詞
正確方法GET、POST、PUT、PATCH、DELETE
適當狀態碼200、201、204、4xx、5xx
一致格式統一的回應結構
分頁排序查詢參數

> **黃金法則**:API 應該讓開發者**看一眼就知道怎麼用**。


進階挑戰

  1. 設計一個電商 API,包含商品、訂單、用戶、購物車。
  2. 研究 JSON:API 規範,與你的設計比較。
  3. 實作一個支援 HATEOAS 的 API。

延伸閱讀與資源