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)

四種模型的關鍵差異

ExpressKoaSpring InterceptorGo wrapper
回到 middleware需要 event hook自然(await next() 後繼續)明確的 postHandle自然(next() 後繼續)
exception 處理需要 4-param error middlewaretry/catch 包 await next()afterCompletion(ex)defer + recover
中止執行不呼叫 next()不呼叫 next()preHandle return false不呼叫 next.ServeHTTP()
程式碼風格callbackasync/awaitOOP interfacefunctional

實際影響:換框架的時候

從 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() 在完整系統裡怎麼運作。