Middleware 執行順序決定一切
Express middleware 是一條線,request 從頭到尾跑過去。順序錯了,結果就錯:
- rate limiter 放在 auth middleware 後面 → 已認證的用戶也能刷爆
- cors 放在 helmet 後面 → preflight request 可能被擋掉
- body parser 放在 auth 後面 → auth middleware 讀不到 body 裡的 token
下面按照建議順序列出,說明每層在做什麼。
第一層:Logging & Request Tracking(最先跑)
morgan / Access Log
每個 request 進來就記一筆 access log。放最前面——否則後面某個 middleware 直接 res.end(),morgan 可能記不到完整的 response。
import morgan from 'morgan';
// dev 模式顯示彩色輸出;production 用 combined 格式接 log aggregator
app.use(morgan('dev'));
// production:接自訂 logger
app.use(morgan('combined', { stream: { write: msg => logger.info(msg.trim()) } }));Request ID(自建)
每個 request 給一個唯一 ID,貫穿整個請求生命週期。要緊接在 morgan 之後——讓 log 和後續追蹤都能帶上 correlation ID。
import { v4 as uuidv4 } from 'uuid';
import { AsyncLocalStorage } from 'node:async_hooks';
export const requestContext = new AsyncLocalStorage<{ requestId: string }>();
app.use((req, res, next) => {
const requestId = (req.headers['x-request-id'] as string) || uuidv4();
res.setHeader('X-Request-Id', requestId);
requestContext.run({ requestId }, next);
});詳見 Structured Logging(correlation ID)。
第二層:Body Parsing & 基礎工具
// JSON body,限制 1mb 防 DoS
app.use(express.json({ limit: '1mb' }));
// URL-encoded form(如果有 form submit 的話)
app.use(express.urlencoded({ extended: true, limit: '1mb' }));cookie-parser
解析 request 的 Cookie header,讓 req.cookies 可用。Session-based auth 和某些 CSRF 方案需要:
import cookieParser from 'cookie-parser';
app.use(cookieParser());response-time
在 response header 加上 X-Response-Time: 12.345ms,可以接進 access log 或監控:
import responseTime from 'response-time';
app.use(responseTime());
// 或自訂 callback
app.use(responseTime((req, res, time) => {
logger.info('Request completed', { path: req.path, duration: time });
}));compression()
gzip 壓縮 response body。
import compression from 'compression';
app.use(compression());file upload 的 route 要用 multer 單獨處理,不走 body parser。
第三層:i18n(Body Parsing 後、Security 前)
i18next-http-middleware 偵測 request 的語系(Accept-Language header、cookie、query param)並注入 req.t 翻譯函式。要在 body parser 後(才能讀 query param),在 auth 前(錯誤訊息也需要翻譯):
import i18next from 'i18next';
import i18nextMiddleware from 'i18next-http-middleware';
// i18next 初始化在 bootstrap 階段完成
app.use(i18nextMiddleware.handle(i18next));
// 之後在任何 middleware / handler 裡:
// req.t('errors.user_not_found') → 自動用 request 的語系翻譯詳見 i18n 設計。
第四層:Security
helmet()
設定安全相關的 HTTP response headers(CSP、X-Frame-Options、HSTS 等)。
import helmet from 'helmet';
app.use(helmet());詳細說明見 API Security。
cors()
處理跨域請求的 preflight 和 headers。
import cors from 'cors';
app.use(cors({
origin: allowedOrigins,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));第五層:Rate Limiting(認證前)
IP-based rate limiting 必須在 auth middleware 之前:
- 目的是擋未認證的濫用(掃描、暴力破解)
- 放在 auth 後面,攻擊者連認證都不需要,可以先用 bot 刷認證 endpoint
import rateLimit from 'express-rate-limit';
// 一般 API:每 15 分鐘 100 次
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
});
// 登入 endpoint:更嚴格(防暴力破解)
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
skipSuccessfulRequests: true, // 登入成功不算在 limit 裡
});
app.use('/api', apiLimiter);
app.use('/auth', authLimiter);詳見 Rate Limiting。
第六層:Maintenance Mode(在 auth 之前擋全站)
系統維護時直接回 503,不需要進 auth 驗證:
app.use((req, res, next) => {
if (process.env.MAINTENANCE_MODE === 'true') {
// 健康檢查 endpoint 例外,讓 load balancer 知道還活著
if (req.path === '/health') return next();
return res.status(503).json({
error: 'Service Unavailable',
message: '系統維護中,請稍後再試',
retryAfter: process.env.MAINTENANCE_UNTIL,
});
}
next();
});第七層:Session(選用)
Session-based auth 才需要。Bearer token / JWT 方案不需要:
import session from 'express-session';
import RedisStore from 'connect-redis';
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 天
sameSite: 'strict',
},
}));JWT 方案用這個位置放 authenticate middleware(詳見下方)。
第八層:Authentication
驗證 JWT / session,把 user 注入 req.user:
const authenticate = async (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload as AuthUser;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
};
// 套在需要認證的 routes 上,不是全站
router.use(authenticate);第九層:Per-Route Middleware(在 router 內)
這些是 route 層級的,不是全站的:
Permission Guard(RBAC)
const requirePermission = (permission: string) => {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user.permissions.includes(permission)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
};
router.delete('/users/:id',
requirePermission('users:delete'),
deleteUserHandler
);Ownership Check(ABAC)
詳見 ABAC 設計。
Request Validation
import { z } from 'zod';
const validate = (schema: z.ZodSchema) => {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation Error',
details: result.error.flatten(),
});
}
req.body = result.data;
next();
};
};詳見 資料綁定與驗證。
第十層:Audit Log(在業務邏輯之後)
有些 audit log 需要知道操作的結果(成功/失敗),所以不能放最前面:
// 用 response interceptor 的方式,在 res.json 被呼叫時觸發
const auditLog = (action: string) => {
return (req: AuthRequest, res: Response, next: NextFunction) => {
const originalJson = res.json.bind(res);
res.json = (body) => {
if (res.statusCode < 400) {
// 只記成功的操作
logger.audit({
actor: req.user?.id,
action,
resource: req.params.id,
ip: req.ip,
});
}
return originalJson(body);
};
next();
};
};
router.delete('/users/:id',
authenticate,
requirePermission('users:delete'),
auditLog('users:delete'), // 放在 guard 之後,業務邏輯之前
deleteUserHandler
);最後層:Static Files & Error Handler
Static Files
如果有靜態資源(前端 build、上傳的圖片),放在 route 之後、error handler 之前:
app.use('/uploads', express.static('uploads'));
app.use('/public', express.static('public'));純 API server 通常不需要這層。
Error Handler
Express 的 error handler 是四個參數的 middleware,放在所有 route 和 middleware 之後:
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
logger.error('Unhandled error', { error: err.message, stack: err.stack });
if (err instanceof AppError) {
return res.status(err.statusCode).json({ error: err.message, code: err.code });
}
res.status(500).json({ error: 'Internal Server Error' });
});詳見 錯誤處理機制。
順序總覽
request 進來
│
├─ morgan() → access log(最先)
├─ requestId → correlation ID
│
├─ express.json() → body parser
├─ express.urlencoded() → form parser
├─ cookieParser() → cookie 解析
├─ responseTime() → 計時
├─ compression() → gzip
│
├─ i18nextMiddleware → 語系偵測(req.t 注入)
│
├─ helmet() → security headers
├─ cors() → cross-origin
│
├─ rateLimit() → IP 限流(認證前!)
├─ maintenanceMode() → 維護模式擋截
│
├─ session() / authenticate → 認證(擇一)
│
├─ [route-level]
│ ├─ permission guard → RBAC
│ ├─ ownership check → ABAC
│ ├─ validate → request 格式驗證
│ └─ auditLog() → 操作記錄(wrap res.json)
│
├─ route handler → 業務邏輯
│
├─ express.static() → 靜態資源(如有)
│
└─ error handler → 統一錯誤回應
第三方套件速查
| 套件 | 用途 | 備註 |
|---|---|---|
morgan | HTTP access log | 接自訂 logger stream |
helmet | Security headers | 必裝 |
cors | 跨域處理 | 必裝 |
compression | gzip 壓縮 | 純 API 效果顯著 |
cookie-parser | Cookie 解析 | session / CSRF 需要 |
response-time | X-Response-Time header | 接進 log / metrics |
express-rate-limit | IP rate limiting | Redis store 版可跨 pod |
express-session | Session-based auth | JWT 方案不需要 |
i18next-http-middleware | 多語系 req.t 注入 | 放在 body parser 後 |
multer | File upload | 只用在 upload route,不走全站 body parser |
express-validator | Route-level 驗證 | 也可用 Zod 取代 |
