本文 AI 產出,尚未審核

TypeScript 中的 Middleware 型別宣告(Node.js + TypeScript 實務應用)

簡介

在 Node.js 生態系統裡,Middleware 是建構可組合、可重用的請求處理流程的核心概念。無論是 Express、Koa、NestJS,甚至是自訂的微服務框架,都依賴 Middleware 來完成驗證、日誌、錯誤處理、資料轉換等工作。

當我們把 TypeScript 引入到這些框架時,最重要的挑戰之一就是正確地為 Middleware 定義型別。只有在編譯階段就能捕捉參數錯誤、回傳值不符合預期,才能真正發揮 TypeScript 提供的安全性與開發效率。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步建立 可讀、可維護且型別安全的 Middleware,讓你的 Node.js 專案在 TypeScript 的加持下更穩定、更易於擴充。


核心概念

1. Middleware 的基本形態

在大多數框架中,Middleware 的簽名大致如下:

(req: Request, res: Response, next: NextFunction) => void | Promise<void>
  • reqres 為 HTTP 請求與回應物件。
  • next 為呼叫下一個 Middleware 的函式。
  • 允許同步非同步(回傳 Promise)兩種寫法。

重點:在 TypeScript 中,我們需要把這三個參數的型別寫清楚,才能讓 IDE 正確提示屬性與方法。


2. 為 Express 撰寫型別安全的 Middleware

2.1 基本型別宣告

import { Request, Response, NextFunction } from 'express';

/** 通用的 Express Middleware 型別 */
export type ExpressMiddleware = (
  req: Request,
  res: Response,
  next: NextFunction
) => void | Promise<void>;

這個 ExpressMiddleware 可以直接在任何自訂的 Middleware 中使用,讓函式簽名保持一致。

2.2 範例:日誌 Middleware

import { ExpressMiddleware } from './types';

export const logger: ExpressMiddleware = (req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
  // 必須呼叫 next(),否則請求會卡住
  next();
};

說明:若忘記呼叫 next(),TypeScript 雖然不會直接報錯,但在執行階段會導致請求懸掛。這就是 型別安全只能保護語法層面,仍需留意邏輯正確性。

2.3 範例:驗證 JWT

import { ExpressMiddleware } from './types';
import jwt from 'jsonwebtoken';

export const authenticate: ExpressMiddleware = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ message: 'Missing token' });
  }

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET!);
    // 把 payload 加到 req 上,需自行擴充 Request 型別
    (req as any).user = payload;
    next();
  } catch (err) {
    return res.status(403).json({ message: 'Invalid token' });
  }
};

技巧process.env.JWT_SECRET! 後面的 ! 告訴 TypeScript 我們確定環境變數一定存在,避免 undefined 警告。若想更安全,可自行寫一個 getEnv 函式做檢查。

2.4 範例:非同步錯誤處理 Middleware

import { ExpressMiddleware } from './types';

export const asyncWrapper = (fn: ExpressMiddleware): ExpressMiddleware => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

// 使用方式
app.get(
  '/users/:id',
  asyncWrapper(async (req, res) => {
    const user = await UserModel.findById(req.params.id);
    if (!user) {
      return res.status(404).json({ message: 'User not found' });
    }
    res.json(user);
  })
);

重點asyncWrapper 把所有非同步錯誤都導向 Express 的錯誤處理 Middleware,避免忘記 try/catch


3. 為 Koa 撰寫型別安全的 Middleware

Koa 的 Middleware 使用 ctx(Context)next,且支援 async/await,簽名如下:

(ctx: Koa.Context, next: Koa.Next) => Promise<any>

3.1 型別宣告

import Koa from 'koa';

export type KoaMiddleware = (ctx: Koa.Context, next: Koa.Next) => Promise<any>;

3.2 範例:請求計時 Middleware

import { KoaMiddleware } from './koa-types';

export const timer: KoaMiddleware = async (ctx, next) => {
  const start = Date.now();
  await next(); // 必須 await,才能在下游完成後繼續執行
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
};

3.3 範例:自訂錯誤處理

export const errorHandler: KoaMiddleware = async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.status = err.status || 500;
    ctx.body = { message: err.message || 'Internal Server Error' };
    ctx.app.emit('error', err, ctx); // 讓全域 error 監聽器也能收到
  }
};

4. 泛型 Middleware:與自訂屬性結合

在實務開發中,我們常會在 req(或 ctx)上掛載自訂屬性,例如 userrequestId。若不額外宣告型別,會失去 TypeScript 的提示。

4.1 Express 的擴充範例

// src/types/express.d.ts
import 'express-serve-static-core';

declare module 'express-serve-static-core' {
  interface Request {
    /** 由 JWT 驗證後注入的使用者資訊 */
    user?: { id: string; role: string };
    /** 追蹤請求的唯一 ID */
    requestId?: string;
  }
}

加入上述宣告後,於 Middleware 中即可安全存取:

export const requestId: ExpressMiddleware = (req, _res, next) => {
  req.requestId = crypto.randomUUID();
  next();
};

export const authorize = (roles: string[]): ExpressMiddleware => {
  return (req, res, next) => {
    if (!req.user || !roles.includes(req.user.role)) {
      return res.status(403).json({ message: 'Forbidden' });
    }
    next();
  };
};

4.2 Koa 的擴充範例

// src/types/koa.d.ts
import 'koa';

declare module 'koa' {
  interface DefaultState {
    /** 由驗證 Middleware 注入的使用者資訊 */
    user?: { id: string; permissions: string[] };
  }
}

使用方式:

export const permission = (required: string): KoaMiddleware => async (ctx, next) => {
  const perms = ctx.state.user?.permissions ?? [];
  if (!perms.includes(required)) {
    ctx.status = 403;
    ctx.body = { message: 'Permission denied' };
    return;
  }
  await next();
};

常見陷阱與最佳實踐

常見問題 為何會發生 建議解法
忘記 await next() (Koa) Koa Middleware 必須等待下游完成,否則後續程式碼會提前執行或根本不執行。 使用 async/await,或在 lint 規則 (eslint-plugin-promise) 中加入 await-promise
next 未被呼叫 (Express) 同步 Middleware 若忘記 next(),請求會卡住。 建議把所有同步 Middleware 包裝成 asyncWrapper,即使不使用 async,也能保證 next 必被呼叫。
自訂屬性缺少型別擴充 直接在 req.user 上賦值會得到 any,失去型別安全。 使用 module augmentation 為框架原始型別加入自訂屬性(如上面的 express.d.tskoa.d.ts)。
錯誤未傳遞至錯誤處理 Middleware 在 async Middleware 中拋出錯誤卻未 catch,Express 只會捕捉同步錯誤。 使用 asyncWrapper 包裝所有 async Middleware,或在 Express 5+ 直接使用 async 函式(Express 5 內建支援)。
過度使用 any 為了快速開發而把所有參數改成 any,導致 IDE 失去提示。 盡量使用框架提供的型別,必要時才使用 泛型自訂型別

最佳實踐清單

  1. 統一型別別名:在專案根目錄建立 types/middleware.d.ts,集中管理 ExpressMiddlewareKoaMiddleware 等。
  2. 使用 asyncWrapper:所有非同步 Middleware 必須包在此函式裡,保證錯誤向上傳遞。
  3. Lint + Prettier:加入 eslint-plugin-import, eslint-plugin-promise,自動檢查未呼叫 next、未 await 的情況。
  4. 單元測試:使用 supertest + jest,對每個 Middleware 撰寫獨立測試,確保型別與行為同步。
  5. 文件化:每個 Middleware 前加上 JSDoc,說明參數、返回值與副作用,IDE 能直接顯示說明。
/**
 * 記錄請求資訊的 Middleware
 * @param req - Express Request
 * @param res - Express Response
 * @param next - 下一個 Middleware
 */
export const logger: ExpressMiddleware = (req, res, next) => { /* ... */ };

實際應用場景

1. 微服務 API Gateway

在大型微服務架構中,API Gateway 常以 Express/Koa 為底層,負責統一驗證、流量控制、日誌、錯誤轉譯等。透過 TypeScript 的 Middleware 型別,我們可以:

  • 把驗證、授權、速率限制分離成獨立套件,每個套件都有自己的型別宣告。
  • 在聚合回應前,使用 asyncWrapper 確保所有非同步錯誤都被捕捉,避免單一服務失效導致整條鏈路崩潰。
app.use(requestId);
app.use(logger);
app.use(authenticate);
app.use(rateLimiter);
app.use(errorHandler);

2. 企業內部系統的 RBAC(Role‑Based Access Control)

透過 自訂 req.user 型別,配合 authorize Middleware,我們可以在路由層級直接寫:

app.get('/admin/users', authorize(['admin']), async (req, res) => {
  const users = await UserModel.findAll();
  res.json(users);
});

此寫法在編譯時即能檢查 authorize 需要的角色是否正確,減少因手寫字串導致的授權錯誤。

3. Server‑Side Rendering (SSR) 與資料預取

在使用 Next.js(Node.js + TypeScript)或自建的 SSR 框架時,常會在 getServerSideProps 前置一段 資料快取或驗證 Middleware。型別宣告可保證:

  • ctx.req 仍保有自訂屬性(如 user)。
  • next 必須回傳 Promise<{ props: any }>,避免因錯誤回傳導致頁面崩潰。
export const withSession: NextMiddleware = async (ctx, next) => {
  const session = await getSession(ctx.req);
  if (!session) {
    return { redirect: { destination: '/login', permanent: false } };
  }
  ctx.req.session = session; // 型別已在 next.d.ts 中擴充
  return next();
};

總結

  • Middleware 是 Node.js 框架的核心,在 TypeScript 中為其正確宣告型別,能在編譯階段即捕捉錯誤、提升開發體驗。
  • 透過 型別別名ExpressMiddlewareKoaMiddleware)與 module augmentation,我們可以安全地在 reqctx 上掛載自訂屬性,如 userrequestId
  • asyncWrapper 為非同步 Middleware 的最佳防護罩,確保所有例外都能傳遞至錯誤處理層。
  • 常見的陷阱包括忘記 next()、未 await next()、以及過度使用 any;遵循 Lint 規則、單元測試、文件化 三大實踐,可有效降低這些問題。
  • API Gateway、RBAC、SSR 等實務場景中,型別安全的 Middleware 能讓系統更易於維護、擴充與除錯。

掌握了 Middleware 型別宣告,你就能在 TypeScript + Node.js 的開發環境中,寫出既 安全可讀 的程式碼,為團隊帶來更高的開發效率與產品品質。祝開發順利! 🚀