本文 AI 產出,尚未審核
ExpressJS (TypeScript) – 專案結構設計:src、routes、controllers、middlewares 分層架構
簡介
在使用 ExpressJS 搭配 TypeScript 開發 API 時,最容易忽略的就是 專案結構。如果把所有程式碼都塞在 app.ts 或 index.ts,隨著功能增多,檔案會變得又長又難以維護,除錯成本也會急速上升。
採用 分層架構(Layered Architecture)——把程式碼依照職責切分成 src、routes、controllers、middlewares 四大層級,能讓:
- 程式碼可讀性提升,開發者快速定位要修改的檔案。
- 測試變得更容易,因為每層只關注自己的輸入與輸出。
- 團隊協作更順暢,大家可以同時在不同層級上工作,互不干擾。
本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步建立一套 乾淨、可擴充 的 Express + TypeScript 專案結構。
核心概念
1. src – 專案根目錄
src(source)是所有程式碼的入口,通常會放置以下幾類檔案:
| 子目錄 | 目的 |
|---|---|
routes/ |
定義 URL 路由與對應的 controller |
controllers/ |
處理業務邏輯,回傳資料或錯誤 |
middlewares/ |
前置/後置處理(驗證、錯誤捕捉、日誌等) |
models/ |
(可選)資料庫模型或 DTO |
config/ |
環境變數、設定檔 |
utils/ |
通用工具函式 |
Tip:在
src內部盡量避免出現「混合」的程式碼,例如把路由直接寫在app.ts,而是把app.ts只留給 應用程式啟動 與 全域中介層。
2. routes – 路由層
路由層的職責是 把 HTTP 請求映射到對應的 controller,不應該包含任何業務邏輯。這樣的好處是:
- URL 結構清晰,修改路由不會意外影響業務處理。
- 可在同一個路由檔案中集中掛載相關的 middleware。
範例 1:src/routes/user.route.ts
import { Router } from 'express';
import { getAllUsers, getUserById, createUser } from '../controllers/user.controller';
import { validateUser } from '../middlewares/validation.middleware';
import { authGuard } from '../middlewares/auth.middleware';
const router = Router();
/**
* GET /users
* 取得全部使用者,需先通過驗證
*/
router.get('/', authGuard, getAllUsers);
/**
* GET /users/:id
* 取得單一使用者
*/
router.get('/:id', authGuard, getUserById);
/**
* POST /users
* 建立新使用者,先執行資料驗證
*/
router.post('/', authGuard, validateUser, createUser);
export default router;
重點:路由檔只負責 組裝,所有實際的處理行為交給 controller。
3. controllers – 控制器層
控制器層是 業務邏輯的入口,負責:
- 解析
req(例如參數、查詢字串、Body)。 - 呼叫 Service/Repository(若有)或直接與資料庫互動。
- 回傳適當的 HTTP 回應(status、payload)。
範例 2:src/controllers/user.controller.ts
import { Request, Response, NextFunction } from 'express';
import { UserModel } from '../models/user.model';
/**
* 取得全部使用者
*/
export const getAllUsers = async (req: Request, res: Response, next: NextFunction) => {
try {
const users = await UserModel.find(); // 假設使用 Mongoose
res.json({ data: users });
} catch (err) {
next(err); // 交給錯誤中介層處理
}
};
/**
* 依 ID 取得單一使用者
*/
export const getUserById = async (req: Request, res: Response, next: NextFunction) => {
const { id } = req.params;
try {
const user = await UserModel.findById(id);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.json({ data: user });
} catch (err) {
next(err);
}
};
/**
* 建立新使用者
*/
export const createUser = async (req: Request, res: Response, next: NextFunction) => {
const payload = req.body;
try {
const newUser = await UserModel.create(payload);
res.status(201).json({ data: newUser });
} catch (err) {
next(err);
}
};
技巧:所有非同步操作都使用
async/await,並在catch中呼叫next(err),讓全域錯誤中介層統一處理。
4. middlewares – 中介層
中介層可以分為 全域中介層(在 app.ts 中掛載)與 路由專屬中介層(在路由檔裡使用)。常見類型:
| 類型 | 範例 |
|---|---|
驗證 (validation.middleware) |
檢查 request body 是否符合 DTO |
授權 (auth.middleware) |
判斷 JWT 是否有效、權限是否足夠 |
日誌 (logger.middleware) |
記錄每筆請求的 method、url、執行時間 |
錯誤處理 (error.middleware) |
捕捉未處理的例外,回傳統一格式的錯誤訊息 |
範例 3:src/middlewares/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { JWT_SECRET } from '../config/constants';
export const authGuard = (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Missing or invalid token' });
}
const token = authHeader.split(' ')[1];
try {
const payload = jwt.verify(token, JWT_SECRET) as { userId: string };
// 把使用者資訊掛到 req,供後續 controller 使用
(req as any).user = { id: payload.userId };
next();
} catch (err) {
return res.status(401).json({ message: 'Invalid token' });
}
};
範例 4:src/middlewares/validation.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { validationResult, checkSchema } from 'express-validator';
export const userSchema = {
name: {
in: ['body'],
isString: true,
notEmpty: true,
errorMessage: 'Name is required and must be a string',
},
email: {
in: ['body'],
isEmail: true,
errorMessage: 'Valid email is required',
},
password: {
in: ['body'],
isLength: { options: { min: 6 } },
errorMessage: 'Password must be at least 6 characters',
},
};
export const validateUser = [
checkSchema(userSchema),
(req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
next();
},
];
範例 5:全域錯誤中介層 src/middlewares/error.middleware.ts
import { Request, Response, NextFunction } from 'express';
export const errorHandler = (
err: any,
_req: Request,
res: Response,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_next: NextFunction,
) => {
console.error('[Error]', err);
const status = err.status || 500;
const message = err.message || 'Internal Server Error';
res.status(status).json({ error: { message } });
};
常見陷阱與最佳實踐
| 陷阱 | 說明 | 最佳實踐 |
|---|---|---|
| 把路由寫在 controller 裡 | 會讓 controller 變得過於龐大,難以重用。 | 保持路由只負責*映射,所有邏輯放在 controller。* |
中介層寫死在 app.ts |
所有路由都會套用相同中介層,導致不必要的效能損耗。 | 在路由檔中針對特定路徑掛載需要的 middleware。 |
| 缺少類型定義 | TypeScript 的好處被浪費,執行時才發現錯誤。 | 為 req.body、req.params 等自訂屬性建立介面(如 interface AuthenticatedRequest extends Request { user: { id: string } })。 |
| 錯誤未傳遞至全域 errorHandler | 程式直接 crash,無法回傳統一錯誤格式。 | 在每個 async controller 中 catch (err) { next(err); },或使用 express-async-errors 套件自動處理。 |
| 路由檔案過長 | 單一檔案管理太多路由,維護成本升高。 | 依功能模組切分路由檔案,例如 user.route.ts、auth.route.ts、product.route.ts。 |
其他最佳實踐
- 使用
tsconfig.json的paths:設定@routes/*、@controllers/*等別名,讓 import 更簡潔。 - 將共用的 DTO/Schema 放在
src/dto:保持驗證規則與 Type 定義同步。 - 測試優先:在
tests/中以單元測試驗證每個 controller、middleware 的行為,確保層級分離的正確性。 - 日誌統一:使用
winston或pino建立全域日誌,並在 middleware 中加入請求 ID,方便追蹤。
實際應用場景
1. 電子商務平台的商品 API
- routes/product.route.ts:只負責
/products、/products/:id的路由設定。 - controllers/product.controller.ts:呼叫
productService完成 CRUD,回傳 JSON。 - middlewares/auth.middleware.ts:只有
POST/PUT/DELETE需要授權,讀取操作則公開。 - middlewares/cache.middleware.ts:對
GET /products加入 Redis 快取,提高讀取效能。
2. 多租戶 SaaS 系統的認證流程
- routes/auth.route.ts:提供
/login、/register、/refresh-token。 - middlewares/tenant.middleware.ts:根據
Host或X-Tenant-ID判斷當前租戶,將租戶資訊寫入req.tenant。 - controllers/auth.controller.ts:使用
req.tenant產生對應租戶的 JWT。 - middlewares/rate-limit.middleware.ts:針對每個租戶設定不同的請求上限,防止資源濫用。
3. 內部管理系統的統一錯誤回報
- middlewares/error.middleware.ts:捕捉所有例外,格式化為
{ error: { code, message, details } },前端只要根據code做相應提示。 - *controllers/
**:拋出自訂AppError(繼承自Error)並帶有status、code`,讓 errorHandler 能正確回傳。
總結
- 分層架構是 Express + TypeScript 專案的基石,
src、routes、controllers、middlewares各司其職,讓程式碼保持 單一職責(SRP)。 - 路由層只負責 URL ↔ controller 的映射;控制器層承擔業務邏輯與資料存取;中介層提供驗證、授權、日誌、錯誤處理等橫切關注點。
- 避免常見陷阱(如把業務邏輯寫在路由、錯誤未傳遞),並遵循 別名 import、型別安全、單元測試、統一日誌 等最佳實踐,能大幅提升開發效率與系統可維護性。
- 在實務中,根據功能模組切分路由與 controller,配合租戶、快取、速率限制等中介層,即可快速構建 可擴充、可靠 的後端服務。
掌握了上述結構與技巧,你的 Express + TypeScript 專案將不再是「雜亂的程式碼堆」,而是一個 清晰、易於測試、可持續發展 的系統。祝開發順利,寫出更好的 API!