本文 AI 產出,尚未審核

ExpressJS (TypeScript) – 專案結構設計:srcroutescontrollersmiddlewares 分層架構


簡介

在使用 ExpressJS 搭配 TypeScript 開發 API 時,最容易忽略的就是 專案結構。如果把所有程式碼都塞在 app.tsindex.ts,隨著功能增多,檔案會變得又長又難以維護,除錯成本也會急速上升。
採用 分層架構(Layered Architecture)——把程式碼依照職責切分成 srcroutescontrollersmiddlewares 四大層級,能讓:

  1. 程式碼可讀性提升,開發者快速定位要修改的檔案。
  2. 測試變得更容易,因為每層只關注自己的輸入與輸出。
  3. 團隊協作更順暢,大家可以同時在不同層級上工作,互不干擾。

本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步建立一套 乾淨、可擴充 的 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.bodyreq.params 等自訂屬性建立介面(如 interface AuthenticatedRequest extends Request { user: { id: string } })。
錯誤未傳遞至全域 errorHandler 程式直接 crash,無法回傳統一錯誤格式。 在每個 async controller 中 catch (err) { next(err); },或使用 express-async-errors 套件自動處理。
路由檔案過長 單一檔案管理太多路由,維護成本升高。 依功能模組切分路由檔案,例如 user.route.tsauth.route.tsproduct.route.ts

其他最佳實踐

  1. 使用 tsconfig.jsonpaths:設定 @routes/*@controllers/* 等別名,讓 import 更簡潔。
  2. 將共用的 DTO/Schema 放在 src/dto:保持驗證規則與 Type 定義同步。
  3. 測試優先:在 tests/ 中以單元測試驗證每個 controller、middleware 的行為,確保層級分離的正確性。
  4. 日誌統一:使用 winstonpino 建立全域日誌,並在 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:根據 HostX-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)並帶有 statuscode`,讓 errorHandler 能正確回傳。

總結

  • 分層架構是 Express + TypeScript 專案的基石,srcroutescontrollersmiddlewares 各司其職,讓程式碼保持 單一職責(SRP)。
  • 路由層只負責 URL ↔ controller 的映射;控制器層承擔業務邏輯與資料存取;中介層提供驗證、授權、日誌、錯誤處理等橫切關注點。
  • 避免常見陷阱(如把業務邏輯寫在路由、錯誤未傳遞),並遵循 別名 import、型別安全、單元測試、統一日誌 等最佳實踐,能大幅提升開發效率與系統可維護性。
  • 在實務中,根據功能模組切分路由與 controller,配合租戶、快取、速率限制等中介層,即可快速構建 可擴充、可靠 的後端服務。

掌握了上述結構與技巧,你的 Express + TypeScript 專案將不再是「雜亂的程式碼堆」,而是一個 清晰、易於測試、可持續發展 的系統。祝開發順利,寫出更好的 API!