Express + TypeScript 型別設計
簡介
在 Node.js 生態系統中,Express 是最常見的 Web 框架,而 TypeScript 則提供靜態型別、編譯時錯誤檢查與更好的 IDE 體驗。把兩者結合,不只可以寫出結構清晰、可維護的程式碼,還能在開發階段即捕捉到許多常見的錯誤,提升團隊的開發效率與程式品質。
本篇文章針對 Express + TypeScript 的型別設計進行實務說明,從專案初始化、路由型別、請求/回應物件的擴充,到錯誤處理與中介軟體(middleware)的型別寫法,都提供具體範例與最佳實踐,協助讀者在日常開發中快速上手並避免常見陷阱。
核心概念
1️⃣ 初始化 TypeScript + Express 專案
先建立一個最小的專案結構,再透過 ts-node-dev 或 nodemon 讓 TypeScript 直接在開發環境執行。
# 建立目錄
mkdir ts-express-demo && cd ts-express-demo
# 初始化 npm、安裝依賴
npm init -y
npm i express
npm i -D typescript ts-node-dev @types/node @types/express
# 產生 tsconfig.json(建議使用 strict 模式)
npx tsc --init --strict
package.json 中加入啟動腳本:
{
"scripts": {
"dev": "ts-node-dev --respawn src/index.ts"
}
}
Tip:
--respawn讓程式在檔案變動時自動重新啟動,開發時非常便利。
2️⃣ 基本的 Express 伺服器(型別完整)
// src/index.ts
import express, { Application, Request, Response, NextFunction } from 'express';
const app: Application = express();
const PORT = process.env.PORT ?? 3000;
// 內建 JSON 中介軟體
app.use(express.json());
// 測試路由
app.get('/', (req: Request, res: Response) => {
res.send('Hello TypeScript + Express!');
});
// 全域錯誤處理
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error(err);
res.status(500).json({ message: err.message });
});
app.listen(PORT, () => console.log(`🚀 Server listening on ${PORT}`));
Application、Request、Response、NextFunction都是 @types/express 提供的型別,使用它們可以讓 IDE 自動補全 HTTP 方法、路由參數等資訊。
3️⃣ 路由參數與查詢字串的型別
3.1 路由參數(Path Params)
// src/routes/user.ts
import { Router, Request, Response } from 'express';
const router = Router();
/**
* GET /users/:id
* :id 為路由參數,預期為數字
*/
router.get('/:id', (req: Request<{ id: string }>, res: Response) => {
const userId = Number(req.params.id); // 轉型為 number
if (Number.isNaN(userId)) {
return res.status(400).json({ error: 'Invalid user id' });
}
// 假設有 getUserById 函式
// const user = getUserById(userId);
res.json({ id: userId, name: 'John Doe' });
});
export default router;
Request<{ id: string }>透過 泛型 指定params的型別,避免req.params.id被推斷為any。
3.2 查詢字串(Query Params)
// src/routes/search.ts
import { Router, Request, Response } from 'express';
const router = Router();
interface SearchQuery {
q: string; // 關鍵字
page?: number; // 第幾頁,預設 1
limit?: number; // 每頁筆數,預設 10
}
/**
* GET /search?q=xxx&page=2
*/
router.get('/', (req: Request<{}, {}, {}, SearchQuery>, res: Response) => {
const { q, page = 1, limit = 10 } = req.query;
// 直接使用 q、page、limit,型別已被推斷為 string | number
res.json({ q, page, limit });
});
export default router;
Request<{}, {}, {}, SearchQuery>的四個泛型分別對應 Params、ResBody、ReqBody、Query,此寫法讓req.query具備完整型別。
4️⃣ Request Body(DTO)與驗證
在實務開發中,通常會使用 DTO(Data Transfer Object) 來描述請求的 JSON 結構,配合 class-validator 或 zod 進行驗證。
npm i class-validator class-transformer
npm i -D @types/class-validator
// src/dto/CreateUserDto.ts
import { IsEmail, IsInt, IsString, Min, Max } from 'class-validator';
export class CreateUserDto {
@IsString()
name!: string;
@IsEmail()
email!: string;
@IsInt()
@Min(0)
@Max(150)
age!: number;
}
// src/middleware/validation.ts
import { plainToInstance } from 'class-transformer';
import { validate, ValidationError } from 'class-validator';
import { Request, Response, NextFunction } from 'express';
export function validateDto<T>(dtoClass: new () => T) {
return (req: Request, res: Response, next: NextFunction) => {
const dtoObj = plainToInstance(dtoClass, req.body);
validate(dtoObj).then((errors: ValidationError[]) => {
if (errors.length > 0) {
const msgs = errors.map(e => Object.values(e.constraints ?? {})).flat();
return res.status(400).json({ errors: msgs });
}
// 把已驗證過的物件掛在 req.body 上,後續可直接使用正確型別
req.body = dtoObj as unknown as T;
next();
});
};
}
// src/routes/user.ts(續)
import { Router, Request, Response } from 'express';
import { CreateUserDto } from '../dto/CreateUserDto';
import { validateDto } from '../middleware/validation';
const router = Router();
router.post(
'/',
validateDto(CreateUserDto), // ← 中介軟體驗證
(req: Request<{}, {}, CreateUserDto>, res: Response) => {
const { name, email, age } = req.body; // 已是正確型別
// TODO: 實作建立使用者的商業邏輯
res.status(201).json({ id: 1, name, email, age });
}
);
export default router;
重點:使用 DTO + 中介軟體,不僅讓型別更嚴謹,也能在 runtime 捕捉不符合規範的資料,提升 API 的安全性。
5️⃣ 中介軟體(Middleware)的型別
自訂中介軟體的簽名與 Express 內建的 RequestHandler 完全相同,只要正確標註泛型即可。
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
export interface AuthPayload {
userId: number;
role: 'admin' | 'user';
}
/**
* 假設從 Header 取得 JWT,解碼後放入 req.auth
*/
export function authMiddleware(
req: Request,
_res: Response,
next: NextFunction
) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return _res.status(401).json({ message: 'Missing token' });
}
// 這裡省略 JWT 驗證,直接模擬 payload
const payload: AuthPayload = { userId: 123, role: 'admin' };
// 使用 TypeScript 的斷言把自訂屬性加到 req 上
(req as Request & { auth: AuthPayload }).auth = payload;
next();
}
在路由中取用:
router.get(
'/admin',
authMiddleware,
(req: Request & { auth: AuthPayload }, res: Response) => {
if (req.auth.role !== 'admin') {
return res.status(403).json({ message: 'Forbidden' });
}
res.json({ secret: 'admin data' });
}
);
技巧:若大量使用自訂屬性,建議透過 模組擴充(declaration merging)一次性為
express-serve-static-core添加屬性,避免每次都寫斷言。
// src/types/express.d.ts
declare namespace Express {
interface Request {
auth?: AuthPayload;
}
}
加入 tsconfig.json 的 typeRoots 設定後,所有檔案即可直接使用 req.auth。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解法 |
|---|---|---|
any 蔓延 |
直接使用 req.body、req.params,TS 會推斷為 any,失去型別保護。 |
使用 泛型 為 Request 指定 Params、Query、Body 型別,或透過 DTO 中介軟體轉型。 |
express.json() 失效 |
忘記在 app.use(express.json()) 前掛載路由,導致 req.body 為 undefined。 |
確保 JSON 中介軟體 為第一個被掛載的 middleware。 |
| 中介軟體順序錯誤 | 驗證或認證 middleware 放在路由之後,導致未經檢查就執行業務邏輯。 | 按 先驗證/認證 → 再執行業務 的順序掛載。 |
| 錯誤處理不完整 | 只寫 app.use((err, req, res, next) => …),但未在 async route 中 next(err),錯誤會被吞掉。 |
使用 async‑wrapper 或 express-async-errors 套件,讓 async 錯誤自動傳遞至錯誤處理器。 |
| 自訂屬性未宣告 | 在 req 上直接加 req.user,TS 會報錯。 |
透過 module augmentation(如上 express.d.ts)一次性擴充型別。 |
最佳實踐
- 開啟
strict:在tsconfig.json中啟用strict、noImplicitAny,讓型別錯誤更早顯現。 - 使用 DTO + class‑validator:分離驗證與型別,保持控制器簡潔。
- 模組化路由:每個資源(user、product …)各自建立
router,並在index.ts中集中掛載。 - 統一錯誤格式:自訂
Error類別,讓前端只需要處理同一種錯誤結構。 - 單元測試 + 型別檢查:使用
ts-jest或vitest,在測試中同時驗證型別與行為。
實際應用場景
1️⃣ 建立企業級 RESTful API
在大型專案中,常見需求包括 分層架構(Controller → Service → Repository)以及 角色授權。透過上面介紹的型別設計,我們可以:
- Controller:只負責接收
req、回傳res,所有參數已經是正確型別。 - Service:接收 DTO,執行業務邏輯,回傳 Domain Model。
- Repository:與資料庫(如 Prisma、TypeORM)互動,回傳 Entity,型別在整條鏈路上保持一致。
2️⃣ 微服務與訊息佇列
當 Express 作為 API Gateway,需要把請求轉發至 Kafka、RabbitMQ 等訊息佇列時,型別可以保證 訊息結構 與 API 輸入 完全對應,減少序列化/反序列化錯誤。
// src/kafka/producer.ts
import { Kafka, Producer } from 'kafkajs';
import { OrderCreatedEvent } from '../events/OrderCreatedEvent';
const kafka = new Kafka({ clientId: 'order-service', brokers: ['localhost:9092'] });
export const producer: Producer = kafka.producer();
export async function publishOrderCreated(event: OrderCreatedEvent) {
await producer.send({
topic: 'order.created',
messages: [{ value: JSON.stringify(event) }],
});
}
OrderCreatedEvent 以 TypeScript interface 定義,確保所有屬性在產生訊息前已被檢查。
3️⃣ 前端與後端共用型別
利用 ts-node 或 tsc 產出 .d.ts,前端(React、Vue)可直接 import type { User } from '../server/src/types',實現 前後端型別同步,減少 API 變更帶來的錯誤。
總結
Express + TypeScript 的型別設計不只是寫程式時的輔助工具,更是 提升專案可維護性、降低錯誤率 的關鍵。從 專案初始化、路由參數/查詢字串/請求 Body 的嚴謹型別,到 DTO + 中介軟體驗證、自訂中介軟體的型別擴充,每一步都能讓開發者在編譯階段即捕捉潛在問題。
同時,避免 any 洩漏、正確安排 middleware 的順序、利用 module augmentation 為 req 加上自訂屬性,都是在實務開發中常見的陷阱與解法。結合上述最佳實踐,你可以快速構建 企業級的 RESTful API、支援 微服務訊息佇列,甚至與前端共享型別,讓整個開發流程更流暢、可靠。
祝你在 TypeScript 與 Express 的世界中寫出更安全、更易維護的程式碼! 🚀