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>
req、res為 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)上掛載自訂屬性,例如 user、requestId。若不額外宣告型別,會失去 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.ts、koa.d.ts)。 |
| 錯誤未傳遞至錯誤處理 Middleware | 在 async Middleware 中拋出錯誤卻未 catch,Express 只會捕捉同步錯誤。 |
使用 asyncWrapper 包裝所有 async Middleware,或在 Express 5+ 直接使用 async 函式(Express 5 內建支援)。 |
過度使用 any |
為了快速開發而把所有參數改成 any,導致 IDE 失去提示。 |
盡量使用框架提供的型別,必要時才使用 泛型 或 自訂型別。 |
最佳實踐清單
- 統一型別別名:在專案根目錄建立
types/middleware.d.ts,集中管理ExpressMiddleware、KoaMiddleware等。 - 使用
asyncWrapper:所有非同步 Middleware 必須包在此函式裡,保證錯誤向上傳遞。 - Lint + Prettier:加入
eslint-plugin-import,eslint-plugin-promise,自動檢查未呼叫next、未await的情況。 - 單元測試:使用
supertest+jest,對每個 Middleware 撰寫獨立測試,確保型別與行為同步。 - 文件化:每個 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 中為其正確宣告型別,能在編譯階段即捕捉錯誤、提升開發體驗。
- 透過 型別別名(
ExpressMiddleware、KoaMiddleware)與 module augmentation,我們可以安全地在req/ctx上掛載自訂屬性,如user、requestId。 asyncWrapper為非同步 Middleware 的最佳防護罩,確保所有例外都能傳遞至錯誤處理層。- 常見的陷阱包括忘記
next()、未await next()、以及過度使用any;遵循 Lint 規則、單元測試、文件化 三大實踐,可有效降低這些問題。 - 在 API Gateway、RBAC、SSR 等實務場景中,型別安全的 Middleware 能讓系統更易於維護、擴充與除錯。
掌握了 Middleware 型別宣告,你就能在 TypeScript + Node.js 的開發環境中,寫出既 安全 又 可讀 的程式碼,為團隊帶來更高的開發效率與產品品質。祝開發順利! 🚀