本文 AI 產出,尚未審核

Express + TypeScript 型別設計


簡介

在 Node.js 生態系統中,Express 是最常見的 Web 框架,而 TypeScript 則提供靜態型別、編譯時錯誤檢查與更好的 IDE 體驗。把兩者結合,不只可以寫出結構清晰、可維護的程式碼,還能在開發階段即捕捉到許多常見的錯誤,提升團隊的開發效率與程式品質。

本篇文章針對 Express + TypeScript 的型別設計進行實務說明,從專案初始化、路由型別、請求/回應物件的擴充,到錯誤處理與中介軟體(middleware)的型別寫法,都提供具體範例與最佳實踐,協助讀者在日常開發中快速上手並避免常見陷阱。


核心概念

1️⃣ 初始化 TypeScript + Express 專案

先建立一個最小的專案結構,再透過 ts-node-devnodemon 讓 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}`));
  • ApplicationRequestResponseNextFunction 都是 @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-validatorzod 進行驗證。

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.jsontypeRoots 設定後,所有檔案即可直接使用 req.auth


常見陷阱與最佳實踐

陷阱 說明 解法
any 蔓延 直接使用 req.bodyreq.params,TS 會推斷為 any,失去型別保護。 使用 泛型Request 指定 ParamsQueryBody 型別,或透過 DTO 中介軟體轉型。
express.json() 失效 忘記在 app.use(express.json()) 前掛載路由,導致 req.bodyundefined 確保 JSON 中介軟體 為第一個被掛載的 middleware。
中介軟體順序錯誤 驗證或認證 middleware 放在路由之後,導致未經檢查就執行業務邏輯。 先驗證/認證 → 再執行業務 的順序掛載。
錯誤處理不完整 只寫 app.use((err, req, res, next) => …),但未在 async route 中 next(err),錯誤會被吞掉。 使用 async‑wrapperexpress-async-errors 套件,讓 async 錯誤自動傳遞至錯誤處理器。
自訂屬性未宣告 req 上直接加 req.user,TS 會報錯。 透過 module augmentation(如上 express.d.ts)一次性擴充型別。

最佳實踐

  1. 開啟 strict:在 tsconfig.json 中啟用 strictnoImplicitAny,讓型別錯誤更早顯現。
  2. 使用 DTO + class‑validator:分離驗證與型別,保持控制器簡潔。
  3. 模組化路由:每個資源(user、product …)各自建立 router,並在 index.ts 中集中掛載。
  4. 統一錯誤格式:自訂 Error 類別,讓前端只需要處理同一種錯誤結構。
  5. 單元測試 + 型別檢查:使用 ts-jestvitest,在測試中同時驗證型別與行為。

實際應用場景

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-nodetsc 產出 .d.ts,前端(React、Vue)可直接 import type { User } from '../server/src/types',實現 前後端型別同步,減少 API 變更帶來的錯誤。


總結

Express + TypeScript 的型別設計不只是寫程式時的輔助工具,更是 提升專案可維護性、降低錯誤率 的關鍵。從 專案初始化路由參數/查詢字串/請求 Body 的嚴謹型別,到 DTO + 中介軟體驗證自訂中介軟體的型別擴充,每一步都能讓開發者在編譯階段即捕捉潛在問題。

同時,避免 any 洩漏、正確安排 middleware 的順序、利用 module augmentationreq 加上自訂屬性,都是在實務開發中常見的陷阱與解法。結合上述最佳實踐,你可以快速構建 企業級的 RESTful API、支援 微服務訊息佇列,甚至與前端共享型別,讓整個開發流程更流暢、可靠。

祝你在 TypeScript 與 Express 的世界中寫出更安全、更易維護的程式碼! 🚀