從 Router 到 Rate Limiter 的完整實作
一句話總結:概念都懂了,現在來看 code——一個完整的 Express API 長什麼樣子。
結論先講:這篇是前兩篇的 code 版本。如果你對 RESTful 設計原則和認證機制還不清楚,建議先回去看 第一篇 和 第二篇。
RESTful Router:五個 CRUD endpoint 的正確姿勢
先看一個使用者資源的完整 Router。注意每個 endpoint 怎麼對應到 HTTP method 和 URL pattern:
import { Router } from 'express';
import { authenticate } from '../middleware/auth.js';
import { validate } from '../middleware/validate.js';
import { createUserSchema, updateUserSchema } from '../schemas/user.js';
const router = Router();
// 列表 + 分頁 + 過濾 + 排序
router.get('/', authenticate, async (req, res) => {
const { page = 1, limit = 20, status, sort = '-created_at' } = req.query;
const offset = (page - 1) * limit;
const filters = {};
if (status) filters.status = status;
const [users, total] = await Promise.all([
UserService.findAll({ filters, sort, limit, offset }),
UserService.count(filters),
]);
res.json({
data: users,
pagination: {
page: Number(page),
limit: Number(limit),
total,
totalPages: Math.ceil(total / limit),
},
});
});
// 單筆查詢
router.get('/:id', authenticate, async (req, res) => {
const user = await UserService.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: { code: 'USER_NOT_FOUND', message: `使用者 ${req.params.id} 不存在` },
});
}
res.json({ data: user });
});
// 建立(注意 201 不是 200)
router.post('/', authenticate, validate(createUserSchema), async (req, res) => {
const user = await UserService.create(req.body);
res.status(201).json({ data: user });
});
// 部分更新
router.patch('/:id', authenticate, validate(updateUserSchema), async (req, res) => {
const user = await UserService.update(req.params.id, req.body);
if (!user) {
return res.status(404).json({
error: { code: 'USER_NOT_FOUND', message: `使用者 ${req.params.id} 不存在` },
});
}
res.json({ data: user });
});
// 刪除(204 No Content)
router.delete('/:id', authenticate, async (req, res) => {
const deleted = await UserService.delete(req.params.id);
if (!deleted) {
return res.status(404).json({
error: { code: 'USER_NOT_FOUND', message: `使用者 ${req.params.id} 不存在` },
});
}
res.status(204).send();
});
// 巢狀資源:Cursor-based 分頁
router.get('/:id/orders', authenticate, async (req, res) => {
const { cursor, limit = 20 } = req.query;
const orders = await OrderService.findByUserId(req.params.id, { cursor, limit });
res.json({
data: orders.items,
pagination: { nextCursor: orders.nextCursor, hasMore: orders.hasMore },
});
});幾個值得注意的地方:
- 列表跟巢狀資源用不同的分頁策略:使用者列表用 offset-based(需要跳頁),訂單用 cursor-based(即時資料流)
validatemiddleware 擋在 controller 前面,讓參數驗證跟業務邏輯分開- 錯誤格式完全統一,前端只需要一個 interceptor
JWT 認證 Middleware
這段 code 處理三件事:簽發 token、驗證 token、角色授權。
import jwt from 'jsonwebtoken';
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
// 簽發 Token 對
export function generateTokens(user) {
const accessToken = jwt.sign(
{ sub: user.id, email: user.email, role: user.role },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: user.id },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// 認證 Middleware
export function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({
error: { code: 'MISSING_TOKEN', message: '未提供認證 Token' },
});
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, ACCESS_SECRET);
req.user = { id: decoded.sub, email: decoded.email, role: decoded.role };
next();
} catch (err) {
const code = err.name === 'TokenExpiredError' ? 'TOKEN_EXPIRED' : 'INVALID_TOKEN';
const message = err.name === 'TokenExpiredError'
? 'Token 已過期,請使用 Refresh Token 換發'
: 'Token 無效';
return res.status(401).json({ error: { code, message } });
}
}
// 授權 Middleware
export function authorize(...allowedRoles) {
return (req, res, next) => {
if (!req.user || !allowedRoles.includes(req.user.role)) {
return res.status(403).json({
error: { code: 'FORBIDDEN', message: '權限不足' },
});
}
next();
};
}注意 authenticate 跟 authorize 是分開的。認證是「你是誰」,授權是「你能幹嘛」。分開之後你可以靈活組合:
// 所有登入使用者都能看
router.get('/profile', authenticate, handler);
// 只有 admin 能操作
router.delete('/users/:id', authenticate, authorize('admin'), handler);
// admin 跟 manager 都能看
router.get('/reports', authenticate, authorize('admin', 'manager'), handler);Rate Limiter:保護你的 API
一個簡易的記憶體型速率限制器(生產環境建議用 Redis):
const requestCounts = new Map();
export function rateLimit({ windowMs = 60000, max = 100, message = '請求過於頻繁' } = {}) {
setInterval(() => {
const now = Date.now();
for (const [key, record] of requestCounts.entries()) {
if (now - record.windowStart > windowMs) requestCounts.delete(key);
}
}, windowMs);
return (req, res, next) => {
const key = req.ip;
const now = Date.now();
let record = requestCounts.get(key);
if (!record || now - record.windowStart > windowMs) {
record = { count: 0, windowStart: now };
requestCounts.set(key, record);
}
record.count++;
res.set('X-RateLimit-Limit', String(max));
res.set('X-RateLimit-Remaining', String(Math.max(0, max - record.count)));
res.set('X-RateLimit-Reset', String(Math.ceil((record.windowStart + windowMs) / 1000)));
if (record.count > max) {
return res.status(429).json({
error: { code: 'RATE_LIMIT_EXCEEDED', message },
});
}
next();
};
}使用時,全域設一層,敏感端點再加嚴:
// 全域:每分鐘 100 次
app.use(rateLimit({ windowMs: 60000, max: 100 }));
// 登入:每 15 分鐘 5 次
app.use('/v1/auth/login', rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: '登入嘗試過於頻繁,請 15 分鐘後再試',
}));這篇的重點回顧
三段完整的 code:RESTful Router 展示了分頁、過濾、排序和統一錯誤格式;JWT Middleware 展示了認證與授權的分離;Rate Limiter 展示了基本的速率限制。
下一篇是這個系列的最後一篇——API 文件化跟常見陷阱。
系列文章:
- API 設計(一):RESTful 基礎
- API 設計(二):認證機制
- 你在這裡 → API 設計(三):實戰程式碼
- API 設計(四):API 文件化與常見陷阱
「看 code 比看理論快,但不理解理論的 code 都是複製貼上。」