Middleware 要解的問題
每個 request 在到達 handler 之前,通常需要做一些共用的事:
- 記 log(這個請求從哪裡來、花多少時間)
- 解析 auth token(把 user 資訊掛到 request 上)
- 驗證 request body(格式不對就直接回 400)
- 設定 response header(CORS / security headers)
這些邏輯不屬於任何一個 handler,但每個 handler 都需要。Middleware 是把這些邏輯在 handler 執行前後插入的機制。
問題是:「在 handler 前後插入」有幾種不同的實作方式,各有不同的能力。
模型一:Pipeline / Chain(Express)
Express 的 middleware 是單向的 pipeline:
Request → M1 → M2 → M3 → Handler → Response
每個 middleware 收到 (req, res, next),執行完自己的邏輯後呼叫 next() 把控制權傳給下一個:
// Express middleware
const logMiddleware = (req, res, next) => {
const start = Date.now();
console.log(`→ ${req.method} ${req.url}`);
next(); // ← 傳給下一個 middleware
// next() 之後的程式碼不一定能拿到正確的 response 資訊
// 因為 next() 是同步的,response 可能還沒送出
};關鍵特性:next() 調用後,控制流繼續往下走,不會自動回到這個 middleware。如果你想在 response 之後做事(例如記錄 response time),需要用 res.on('finish') event hook:
const responseTimeMiddleware = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`← ${req.method} ${req.url} ${res.statusCode} ${duration}ms`);
});
next();
};模型二:Onion(Koa / Hono)
Koa 的 middleware 是洋蔥模型——控制流可以「進去再出來」:
┌──────────────────────────────┐
│ M1 │
│ ┌──────────────────────┐ │
│ │ M2 │ │
│ │ ┌──────────────┐ │ │
│ │ │ M3 │ │ │
│ │ │ Handler │ │ │
│ │ └──────────────┘ │ │
│ └──────────────────────┘ │
└──────────────────────────────┘
Request →→→→→ M1 → M2 → M3 → Handler → M3 → M2 → M1 →→→→→ Response
await next() 讓你在 handler 執行前做一件事,在 handler 執行後繼續做另一件事:
// Koa middleware
app.use(async (ctx, next) => {
const start = Date.now(); // ← handler 之前
await next(); // ← 等 handler + 後續 middleware 跑完
const duration = Date.now() - start;
ctx.set('X-Response-Time', `${duration}ms`); // ← handler 之後,response 已就緒
});這讓計算 response time、在 response header 加資訊、catch handler 拋出的 exception 都變得非常自然——不需要 event hook。
// 錯誤處理也更直觀
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.statusCode || 500;
ctx.body = { error: err.message };
}
});模型三:Pre/Post 分離(Spring / Laravel / Django)
Spring 的 HandlerInterceptor 把 middleware 分成三個明確的時間點:
public class LoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
// Handler 執行之前
// return false → 中止執行,不進入 handler
// return true → 繼續執行
log.info("→ {} {}", req.getMethod(), req.getRequestURI());
return true;
}
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse res,
Object handler, ModelAndView mv) {
// Handler 執行完,response 還沒寫出去之前
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
Object handler, Exception ex) {
// Response 完全寫出去後(即使有 exception 也會跑)
// 適合做資源清理、response time 記錄
}
}這比 Express 的 pipeline 更結構化——你明確地知道「我在 handler 之前做這個、之後做那個、全部結束後做另一個」,不需要 event hook 或 async await 技巧。
Django middleware 也是同樣概念(process_request / process_response / process_exception)。
模型四:Handler Wrapper(Go stdlib / Fiber)
Go 沒有 Express-style 的 middleware API,慣用寫法是函式包函式:
// 定義 middleware 函式
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // ← 呼叫下一層
log.Printf("← %s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
// 使用
mux := http.NewServeMux()
mux.HandleFunc("/users", UserHandler)
// 包起來
handler := LoggingMiddleware(AuthMiddleware(mux))
http.ListenAndServe(":8080", handler)這是函式組合(functional composition)——LoggingMiddleware(AuthMiddleware(mux)) 讓執行順序:Logging → Auth → Handler → Auth 之後 → Logging 之後。
Gin 對這個模式做了封裝,讓它更像 Express:
r := gin.New()
r.Use(LoggingMiddleware())
r.Use(AuthMiddleware())
r.GET("/users", UserHandler)四種模型的關鍵差異
| Express | Koa | Spring Interceptor | Go wrapper | |
|---|---|---|---|---|
| 回到 middleware | 需要 event hook | 自然(await next() 後繼續) | 明確的 postHandle | 自然(next() 後繼續) |
| exception 處理 | 需要 4-param error middleware | try/catch 包 await next() | afterCompletion(ex) | defer + recover |
| 中止執行 | 不呼叫 next() | 不呼叫 next() | preHandle return false | 不呼叫 next.ServeHTTP() |
| 程式碼風格 | callback | async/await | OOP interface | functional |
實際影響:換框架的時候
從 Express 換 Koa:你的 middleware 邏輯基本可以平移,但要把 event hook 改成 await next() 後直接寫。直覺上更簡單。
從 Express 換 NestJS:NestJS 的 middleware 支援 Express 風格(next()),但它的 Guard / Interceptor / Pipe 是更結構化的版本。Guard = 決定要不要讓請求通過;Interceptor = request/response 轉換(對應 Koa onion);Pipe = request validation。這四個層次在 Express 裡都是「middleware」,NestJS 把它們分開命名了。
從 Express 換 Spring:你要學 @Component 的 Interceptor 方式,但 pre/post/afterCompletion 的直覺很強,debug 也更容易。
Express 的 middleware 實作
[[backend/framework/express/middleware|[express][M5] Middleware 架構:4 階段掛載]] 有 proto 的完整 middleware 設計,對照這篇的「Pipeline 模型」理解 Express 的 next() 在完整系統裡怎麼運作。
