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_profiles2.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 | 說明 |
|---|---|---|---|
| Create | POST | /users | 創建資源 |
| Read | GET | /users, /users/1 | 讀取資源 |
| Update | PUT | /users/1 | 完整更新 |
| Update | PATCH | /users/1 | 部分更新 |
| Delete | DELETE | /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 Unavailable4.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 應該讓開發者**看一眼就知道怎麼用**。
進階挑戰
- 設計一個電商 API,包含商品、訂單、用戶、購物車。
- 研究 JSON:API 規範,與你的設計比較。
- 實作一個支援 HATEOAS 的 API。