從 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(即時資料流)
  • validate middleware 擋在 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();
  };
}

注意 authenticateauthorize 是分開的。認證是「你是誰」,授權是「你能幹嘛」。分開之後你可以靈活組合:

// 所有登入使用者都能看
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 文件化跟常見陷阱。

系列文章:

「看 code 比看理論快,但不理解理論的 code 都是複製貼上。」