本文 AI 產出,尚未審核
ExpressJS (TypeScript)
單元:建立 HTTP Server
主題:使用 TypeScript Interface 描述資料格式
簡介
在 Node.js 生態系中,Express 是最常被採用的 Web 框架之一。當我們把 TypeScript 帶入 Express 專案時,最能體現「型別安全」的地方,就是 資料格式的描述。無論是 Request Body、Query String,或是 Response 的結構,都可以透過 interface 事先定義,讓編譯器在開發階段即捕捉錯誤,降低跑到生產環境才發現問題的風險。
本篇文章將帶你從最基礎的 interface 語法,逐步延伸到 Express 中的實作方式,並提供多個實用範例,說明如何在 HTTP Server 內部以型別描述資料,讓程式碼更易讀、可維護,同時也提升 API 的可靠度。
核心概念
1️⃣ 為什麼要用 Interface 描述資料格式?
- 型別安全:編譯期即能偵測屬性遺漏或型別不符。
- 自動補完:IDE(如 VS Code)會根據介面提供屬性名稱與型別的提示,大幅提升開發效率。
- 文件化:介面本身即是資料結構的說明文件,讓團隊成員快速了解 API 輸入/輸出規範。
Tip:在大型專案中,將所有介面集中於
src/types/或src/interfaces/資料夾,方便統一管理與重用。
2️⃣ 基本語法與範例
// src/interfaces/User.ts
export interface User {
/** 使用者唯一識別碼 */
id: string;
/** 使用者名稱 */
name: string;
/** 年齡(可選) */
age?: number;
/** 電子信箱 */
email: string;
}
?表示屬性為可選。- 介面可以 繼承 其他介面,形成更複雜的型別結構。
export interface Admin extends User {
/** 管理員權限等級 */
role: 'super' | 'moderator';
}
3️⃣ 在 Express 中使用 Interface 描述 Request Body
// src/routes/user.ts
import { Router, Request, Response } from 'express';
import { User } from '../interfaces/User';
const router = Router();
/**
* 建立新使用者
* POST /users
*/
router.post('/users', (req: Request<{}, {}, User>, res: Response) => {
// 直接把 req.body 當作 User 介面使用
const newUser: User = req.body;
// 假設此處有資料庫寫入邏輯
// db.saveUser(newUser);
res.status(201).json({ message: '使用者建立成功', user: newUser });
});
export default router;
Request<Params, ResBody, ReqBody, Query>:Express 的Request型別支援四個泛型參數,第三個即是 Request Body 的型別。- 透過此寫法,若前端傳來缺少
email屬性,TypeScript 會在編譯階段報錯(若使用 strict 模式)。
4️⃣ 描述 Query String 與 Route Params
// src/routes/product.ts
import { Router, Request, Response } from 'express';
interface ProductQuery {
/** 分頁索引,預設 1 */
page?: number;
/** 每頁筆數,預設 10 */
limit?: number;
/** 依價格排序:asc 或 desc */
sort?: 'asc' | 'desc';
}
const router = Router();
router.get('/products', (req: Request<{}, {}, {}, ProductQuery>, res: Response) => {
const { page = 1, limit = 10, sort = 'asc' } = req.query;
// 轉換成正確的型別(query 皆為字串)
const pageNum = Number(page);
const limitNum = Number(limit);
// 假設有 getProducts 的服務
// const products = productService.getProducts({ page: pageNum, limit: limitNum, sort });
res.json({ page: pageNum, limit: limitNum, sort, /* products */ });
});
export default router;
- 注意:
req.query預設皆為字串,需要自行轉型(如Number())。 - 若使用 zod、class-validator 等驗證函式庫,可將介面結合 schema 進行更嚴謹的驗證。
5️⃣ Response 型別的描述
// src/interfaces/ApiResponse.ts
export interface ApiResponse<T> {
/** 狀態碼 */
code: number;
/** 訊息文字 */
message: string;
/** 真正的資料 */
data: T;
}
// src/routes/auth.ts
import { Router, Request, Response } from 'express';
import { ApiResponse } from '../interfaces/ApiResponse';
import { User } from '../interfaces/User';
const router = Router();
router.post('/login', (req: Request, res: Response) => {
// 假設驗證成功,回傳使用者資訊
const user: User = {
id: 'u123',
name: 'Alice',
email: 'alice@example.com',
};
const response: ApiResponse<User> = {
code: 200,
message: '登入成功',
data: user,
};
res.json(response);
});
export default router;
- 使用 泛型 (
ApiResponse<T>) 能讓回傳結構保持一致,同時保留 data 部分的具體型別資訊。
6️⃣ 搭配 Validation Library(以 class-validator 為例)
// src/dto/CreateUserDto.ts
import { IsEmail, IsNotEmpty, IsOptional, IsString, IsInt, Min } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty()
@IsString()
name!: string;
@IsOptional()
@IsInt()
@Min(0)
age?: number;
@IsEmail()
email!: string;
}
// src/middleware/validation.ts
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { Request, Response, NextFunction } from 'express';
export function validateDto(dtoClass: any) {
return (req: Request, res: Response, next: NextFunction) => {
const dtoObj = plainToInstance(dtoClass, req.body);
validate(dtoObj).then(errors => {
if (errors.length > 0) {
const messages = errors.map(e => Object.values(e.constraints || {})).flat();
res.status(400).json({ message: '參數驗證失敗', errors: messages });
} else {
// 把驗證過的物件掛到 req.body 上,型別已經是 dtoClass
req.body = dtoObj;
next();
}
});
};
}
// src/routes/user.ts(結合驗證與介面)
import { Router, Request, Response } from 'express';
import { validateDto } from '../middleware/validation';
import { CreateUserDto } from '../dto/CreateUserDto';
import { User } from '../interfaces/User';
const router = Router();
router.post(
'/users',
validateDto(CreateUserDto),
(req: Request, res: Response) => {
// 此時 req.body 已經是 CreateUserDto 類型
const dto = req.body as CreateUserDto;
const newUser: User = {
id: 'generated-id',
name: dto.name,
age: dto.age,
email: dto.email,
};
res.status(201).json({ message: '建立成功', user: newUser });
},
);
export default router;
- 好處:
interface描述結構、class-validator負責 runtime 驗證,兩者結合可同時獲得 編譯期安全 與 執行期保護。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記將 req.body 轉型 |
直接使用 any 會失去型別檢查。 |
在路由函式簽名中使用 Request<{}, {}, YourInterface>,或自行 as YourInterface。 |
| Query 參數自動轉型失效 | req.query 皆為字串,直接賦值給 number 會出錯。 |
手動 Number()、parseInt(),或使用 zod、yup 等 schema 轉型。 |
| 介面與 DTO 不一致 | 介面只描述型別,卻未加入驗證規則,導致不符合業務規則的資料通過。 | 配合 class-validator、zod 等驗證庫,將「結構」與「規則」分層管理。 |
過度使用 any |
為了省事直接 any,失去 TypeScript 的好處。 |
在任何可能的地方使用具體介面或泛型。 |
| 介面循環引用 | 兩個介面互相引用會造成編譯錯誤。 | 使用 type 或 interface 前向宣告,或將共同結構抽離成獨立介面。 |
最佳實踐
- 統一管理介面:所有 API 相關的介面放在
src/interfaces/,保持命名規則一致(如User.ts、Product.ts)。 - 結合驗證:使用 DTO(Data Transfer Object)搭配驗證庫,讓「資料結構」與「驗證規則」分離。
- 使用泛型:回傳格式(如
ApiResponse<T>)盡量使用泛型,避免重複寫相同的結構。 - 開啟嚴格模式:在
tsconfig.json中啟用"strict": true,確保所有隱式 any 都被捕捉。 - 自動化測試:利用 supertest + jest 撰寫整合測試,確保介面變更不會破壞既有 API。
實際應用場景
🎯 場景一:會員註冊 API
- 需求:前端送出
name、email、password、age?,後端必須驗證格式並回傳新會員資料。 - 實作:
- 定義
CreateUserDto(驗證)與User(介面)。 - 使用
validateDto中介軟體確保資料正確。 - 回傳
ApiResponse<User>,讓前端能直接使用型別資訊。
- 定義
🎯 場景二:商品列表分頁
- 需求:支援
page、limit、category?, priceRange?,回傳total、items[]。 - 實作:
ProductQuery介面描述 query 參數。PaginatedResult<T>泛型介面描述分頁結果。- 在服務層返回
PaginatedResult<Product>,Controller 只負責型別轉換。
🎯 場景三:第三方 webhook 接收
- 需求:外部服務會 POST 一段 JSON,格式固定(如
eventId、timestamp、payload),但不允許錯誤的欄位。 - 實作:
- 用
WebhookPayload介面完整描述payload結構。 - 透過
express.json()解析後,直接as WebhookPayload,若資料不符,回傳 400。
- 用
總結
使用 TypeScript Interface 來描述 Express API 的資料格式,是提升程式碼品質、降低錯誤率的關鍵一步。透過介面我們可以:
- 在 編譯期 捕捉屬性遺漏或型別不符的問題,減少跑測試的成本。
- 為 IDE 提供完整的自動補完與文件提示,讓開發者快速上手。
- 結合 DTO + 驗證庫,同時兼顧 結構安全 與 業務規則。
在實務上,從 Request Body、Query、Route Params 到 Response,都可以使用介面或泛型進行統一管理。只要遵循「介面分層、驗證分離、嚴格模式」的最佳實踐,便能打造出 可維護、可擴充 的 Express + TypeScript 應用程式。
快把今天學到的 interface 實作方式,套用到你的下一個 API 中吧!祝開發順利 🚀