📋 目錄
上一篇文章學會了 Express,卻不知道自己的 API 設計是否合理?URL 要怎麼命名?什麼時候用 GET、什麼時候用 POST?如何決定 HTTP Status Code?這篇補足多數後端教學忽略的「API 設計」這塊,讓你從「會寫後端」升級到「會設計 API」。
前言:API 設計為什麼重要?
前端工程師最容易踩的坑是:學會了 Express 路由,卻把所有功能都用 POST 解決——反正 request.body 什麼都能傳,何必糾結要用 GET 還是 PUT?
這個心態會造成三個問題:
- 前端開發困難:不同的人呼叫同一個 API,預期結果不一致,不知道該用哪個 method
- 除錯地獄:當有 bug 時,光從 request log 無法判斷「這是查詢還是修改」,很難重現問題
- 擴展性差:好的 API 設計是系統的骨架,爛的 API 會讓系統在需求變化時難以擴展
RESTful 不是銀彈,但它是一套經過大量實踐檢驗的共識。遵循 RESTful convention 的 API,不需要額外文件解釋「這個端點是做什麼的」,名稱本身就自帶語意。
HTTP Methods:不是只有 GET 和 POST
很多人誤以為 HTTP 只有 GET(拿資料)和 POST(送資料)兩種方法。實際上有五種最常用的:
| Method | 語意 | 是否 idempotent | 是否有 request body |
|---|---|---|---|
| GET | 取得資源 | ✅ 是 | ❌ 沒有 |
| POST | 建立資源 | ❌ 否 | ✅ 有 |
| PUT | 完整更新資源(替換) | ✅ 是 | ✅ 有 |
| PATCH | 部分更新資源 | ❌ 否 | ✅ 有 |
| DELETE | 刪除資源 | ✅ 是 | ❌ 沒有 |
Idempotent 的意思是:無論你發一次還是發一百次,結果都一樣。GET 和 DELETE 天然 idempotent,PUT 也是,但 POST 和 PATCH 不是(每次都會建立新資源或產生副作用)。
什麼時候用 PUT vs PATCH?
這是最容易混淆的地方。記住這個原則:
- PUT:替換整筆資源。客戶端必須傳完整的資源內容,缺少任何欄位會被當成「該欄位設為空」
- PATCH:只更新指定欄位。客戶端只傳要改的部分,其餘不動
// PUT /api/users/1 — 完整更新
// 客戶端必須傳完整的使用者資料
{
"name": "小明",
"email": "xiaoming@example.com",
"role": "admin" // 如果漏掉這個欄位,server 會把 role 清除
}
// PATCH /api/users/1 — 部分更新
// 客戶端只傳要改的欄位
{
"email": "new-email@example.com" // 只有 email 會被更新,其餘不動
}
實務建議:大多數部分更新的場景用 PATCH 即可。除非你的系統有明確的「版本控制」需求,必須保留完整歷史,否則 PUT 的使用頻率相對低。
HTTP Status Code:不要只會回 200
後端新手最常見的錯誤,是無論成功失敗都回 200 OK,區別只在 response body 裡的 success 欄位。這樣前端必須每次都 parse body 才能知道結果——HTTP Status Code 的價值就是讓雙方不用看 body 就能知道發生了什麼事。
最常用的幾個 Status Code
成功(2xx):
200 OK:請求成功,response 有內容(GET、PATCH)201 Created:資源建立成功,通常在 POST 之後回傳,Header 會帶Location指向新資源204 No Content:成功但沒有內容要回傳,常用在 DELETE 之後
客戶端錯誤(4xx):
400 Bad Request:請求格式有問題(少了必填欄位、格式錯誤、id 不存在)401 Unauthorized:未認證(沒有登入)403 Forbidden:已認證但沒有權限404 Not Found:資源不存在409 Conflict:資源衝突(例如建立時發現名稱重複)422 Unprocessable Entity:語法正確但語意錯誤(例如 email 格式對但不存在於允許清單)
伺服器錯誤(5xx):
500 Internal Server Error:伺服器端的錯誤,通常是未捕捉的 exception
// Express 中的正確 Status Code 使用
app.post('/api/users', async (req, res) => {
try {
const { name, email } = req.body;
// 簡單的驗證
if (!name || !email) {
return res.status(400).json({ error: 'name 和 email 是必填欄位' });
}
// 檢查 email 格式(簡單示範)
if (!email.includes('@')) {
return res.status(400).json({ error: 'email 格式不正確' });
}
// 建立資源
const newUser = await db.users.create({ name, email });
// 201 + Location header
res.status(201)
.set('Location', `/api/users/${newUser.id}`)
.json(newUser);
} catch (err) {
// 500:不應該發生的錯誤
console.error('Unexpected error:', err);
res.status(500).json({ error: '伺服器錯誤,請稍後再試' });
}
});
app.delete('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: '使用者不存在' });
}
await db.users.delete(req.params.id);
// DELETE 成功不留 body
res.status(204).end();
});
資源命名:URL 就是你的 API 文件
RESTful 的核心思想是:URL 代表「資源」,不是「動作」。
壞的設計:
POST /api/createUser ← 「建立使用者」是動作,不是資源
POST /api/updateUser?id=1 ← updateUser 是動作
GET /api/getUserById?id=1 ← getUserById 是動作
好的設計:
POST /api/users ← 建立資源
GET /api/users/1 ← 取得 id=1 的使用者
PUT /api/users/1 ← 替換 id=1 的使用者
PATCH /api/users/1 ← 部分更新 id=1 的使用者
DELETE /api/users/1 ← 刪除 id=1 的使用者
URL 本身就是文件。看到 /api/users/1 就知道是操作 id=1 的使用者資源,不需要另外說明。
巢狀資源怎麼處理?
如果資源有從屬關係,URL 也可以巢狀:
GET /api/users/1/orders ← 取得 user 1 的所有訂單
GET /api/users/1/orders/5 ← 取得 user 1 的訂單 5
POST /api/users/1/orders ← 為 user 1 建立訂單
但要注意:巢狀層級不要超過兩層,否則 URL 會變得太長太耦合。如果出現三層以上的巢狀,考慮用 query parameter 或把資源拉平:
# 太深的巢狀
GET /api/orgs/1/teams/2/members/3
# 更好的設計:直接用成員 ID
GET /api/members/3
前端視角:API 設計不良的痛苦
身為前端工程師,你一定遇過這種 API:
// 這個 API 是要做什麼?
// 查詢?建立?更新?看不出來
POST /api/handleUser
// 回傳這個到底是成功還是失敗?
{
"code": 0,
"data": { ... },
"message": "ok"
}
好的 RESTful API 讓前端呼叫時語意清晰:
// 查詢使用者——語意清楚
const user = await fetch('/api/users/1').then(r => r.json());
// 建立訂單——回 201,前端就知道是新建成功
const order = await fetch('/api/users/1/orders', {
method: 'POST',
body: JSON.stringify({ amount: 1000 })
}).then(r => r.status === 201 ? r.json() : null);
前端視角的 API 設計原則:
- 可預測:同一個 resource 的操作,用同樣的 URL pattern
- 語意明確:從 URL 和 method 就能知道要幹嘛,不需要文件
- Status Code 有意義:不要讓前端必須 parse body 才能判斷成敗
- 錯誤訊息具體:
400: "email 格式不正確"優於400: "操作失敗"
常見問題
Q:RESTful 是強制的嗎?
A:不是。RESTful 是一種風格,不是規範。很多大型系統(GitHub、Stripe)有自己的 API 設計慣例,不完全符合 RESTful 但仍然非常好用。重點是團隊內部保持一致,有自己的 conventions 寫在文件裡,比強迫遵守 RESTful 更有價值。
Q:如果一個操作涉及多個資源怎麼辦?
A:例如「把使用者移到另一個部門」需要同時更新 User 和 Department。幾種做法:
- 分散式請求:前端分兩次呼叫(先 PATCH User,再更新 Department)
- Batch API:設計
POST /api/batch,一次送多個操作 - Transaction:後端包成一個 transaction,前端一次呼叫
實務上分散式請求最常見,但缺點是中途失敗會造成狀態不一致。重要操作建議用 Batch 或 Transaction。
Q:需要版本控制(v1、v2)嗎?
A:小型專案不需要。版本控制是當 API 要breaking change 但又必須向後相容時才需要的機制。一開始設計好的 API 通常不需要到 v2,過早版本控制只會增加維護負擔。
總結:API 設計是給未來的自己用的
這篇文章的核心只有一個概念:好的 API 設計,是讓別人(或者未來的自己)看見 URL 就能預期會發生什麼。
HTTP Method、Status Code、URL 命名——這些不是學術規定,是減少溝通成本的實務工具。
三個快速檢查清單,下次設計 API 前問自己:
- 這個操作用對 HTTP Method 了嗎?——能用 GET 就不要用 POST
- Status Code 有意義嗎?——成功失敗看 status code 就知道,不需要 parse body
- URL 是資源導向嗎?——URL 代表「東西」,不是「動作」
遵循這些原則,你的 API 會比大多數線上教程的範例還要專業。
本篇是「前端工程師後端入門」系列第二篇。第一篇《從 Node.js 原生 HTTP 到 Express 框架》說明了框架的價值,本篇則聚焦在如何設計 RESTful API。