本文 AI 產出,尚未審核

ExpressJS (TypeScript) – 實戰專案:完整 API 服務

加入 Auth、Validation、Database


簡介

在現代 Web 開發中,API 服務 已成為前後端分離架構的核心。單靠 Express 建立路由與回傳 JSON 已能快速完成原型,但若要在正式環境上線,必須加入 認證(Auth)參數驗證(Validation)資料庫存取 等基礎設施,才能保證安全性、資料完整性與可維護性。

本篇文章以 TypeScript 為基底,示範如何在 Express 專案中:

  1. 使用 JWT 完成使用者驗證與授權
  2. 結合 class‑validatorexpress‑validator 進行請求參數驗證
  3. Prisma(或 TypeORM)作為 ORM,連接 PostgreSQL/MySQL,完成 CRUD

透過完整、可執行的範例,讓剛踏入 Node.js/Express 的開發者,快速掌握「從零到可上線」的 API 服務建置流程。


核心概念

1️⃣ 建立 TypeScript + Express 基礎環境

先安裝必要套件,並設定 tsconfig.json 讓編譯結果位於 dist 目錄。

npm init -y
npm install express cors helmet morgan
npm install -D typescript ts-node-dev @types/express @types/node
npx tsc --init

tsconfig.json 重要設定:

{
  "target": "ES2022",
  "module": "commonjs",
  "outDir": "./dist",
  "rootDir": "./src",
  "strict": true,
  "esModuleInterop": true
}

src/server.ts

import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';

const app = express();

// 基礎中介層
app.use(cors());
app.use(helmet());
app.use(morgan('dev'));
app.use(express.json());

// 測試路由
app.get('/', (req, res) => {
  res.json({ message: 'Hello Express + TypeScript!' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`🚀 Server running on http://localhost:${PORT}`));

小技巧:使用 npm run dev 透過 ts-node-dev 監控檔案變動,自動重啟伺服器。

// package.json scripts
"scripts": {
  "dev": "ts-node-dev --respawn src/server.ts",
  "build": "tsc",
  "start": "node dist/server.js"
}

2️⃣ JWT 認證與授權

2.1 為什麼選 JWT?

  • 無狀態:伺服器不需要維護 Session,適合水平擴展。
  • 可攜帶資訊:在 token 中加入 roleiatexp 等欄位,讓授權判斷更彈性。

2.2 安裝與設定

npm install jsonwebtoken bcryptjs
npm install -D @types/jsonwebtoken @types/bcryptjs

2.3 建立 User Entity(使用 Prisma)

// prisma/schema.prisma
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  password  String
  role      String   @default("user")
  createdAt DateTime @default(now())
}

注意password 必須 加鹽雜湊 後儲存,絕不可明文。

2.4 Auth Service(產生 & 驗證 Token)

// src/services/auth.service.ts
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_EXPIRES_IN = '7d';

export async function register(email: string, password: string) {
  const hashed = await bcrypt.hash(password, 10);
  const user = await prisma.user.create({
    data: { email, password: hashed },
  });
  return generateToken(user);
}

export async function login(email: string, password: string) {
  const user = await prisma.user.findUnique({ where: { email } });
  if (!user) throw new Error('User not found');

  const valid = await bcrypt.compare(password, user.password);
  if (!valid) throw new Error('Invalid credentials');

  return generateToken(user);
}

function generateToken(user: { id: number; email: string; role: string }) {
  const payload = { sub: user.id, email: user.email, role: user.role };
  return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
}

2.5 Auth Middleware

// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';

export interface AuthRequest extends Request {
  user?: { sub: number; email: string; role: string };
}

export function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ message: 'Missing token' });
  }

  const token = authHeader.split(' ')[1];
  try {
    const payload = jwt.verify(token, JWT_SECRET) as { sub: number; email: string; role: string };
    req.user = payload;
    next();
  } catch (err) {
    return res.status(401).json({ message: 'Invalid token' });
  }
}

/** 角色授權範例 */
export function authorize(allowedRoles: string[]) {
  return (req: AuthRequest, res: Response, next: NextFunction) => {
    if (!req.user) return res.status(403).json({ message: 'Forbidden' });
    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({ message: 'Insufficient permission' });
    }
    next();
  };
}

2.6 路由範例

// src/routes/auth.routes.ts
import { Router } from 'express';
import { register, login } from '../services/auth.service';

const router = Router();

router.post('/register', async (req, res) => {
  const { email, password } = req.body;
  try {
    const token = await register(email, password);
    res.json({ token });
  } catch (e) {
    res.status(400).json({ message: (e as Error).message });
  }
});

router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  try {
    const token = await login(email, password);
    res.json({ token });
  } catch (e) {
    res.status(400).json({ message: (e as Error).message });
  }
});

export default router;

server.ts 中掛載:

import authRoutes from './routes/auth.routes';
app.use('/api/auth', authRoutes);

3️⃣ 請求參數驗證(Validation)

3.1 為什麼需要 Validation?

  • 防止 SQL InjectionXSS 等攻擊
  • 保證 API contract,減少前端與後端的溝通成本

3.2 使用 class-validator + class-transformer(DTO)

npm install class-validator class-transformer
npm install -D @types/express-serve-static-core

DTO 範例(UserCreateDto):

// src/dto/user-create.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';

export class UserCreateDto {
  @IsEmail({}, { message: 'Email 必須為有效格式' })
  email!: string;

  @IsString()
  @MinLength(6, { message: '密碼長度至少 6 位' })
  password!: string;
}

驗證中介層

// src/middleware/validation.middleware.ts
import { plainToInstance } from 'class-transformer';
import { validate, ValidationError } from 'class-validator';
import { Request, Response, NextFunction } from 'express';

export function validateDto(dtoClass: any) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const dtoObj = plainToInstance(dtoClass, req.body);
    const errors = await validate(dtoObj);
    if (errors.length > 0) {
      const messages = errors
        .map((err: ValidationError) => Object.values(err.constraints || {}))
        .flat();
      return res.status(400).json({ errors: messages });
    }
    // 把已驗證的 DTO 放回 req.body,型別安全
    req.body = dtoObj;
    next();
  };
}

在路由中使用

// src/routes/auth.routes.ts(續)
import { validateDto } from '../middleware/validation.middleware';
import { UserCreateDto } from '../dto/user-create.dto';

router.post(
  '/register',
  validateDto(UserCreateDto), // <-- 加入驗證
  async (req, res) => {
    const { email, password } = req.body; // 已是 DTO 類型
    // ... 省略
  }
);

3.3 express-validator(快速檢查 query / params)

npm install express-validator
// src/routes/article.routes.ts
import { Router } from 'express';
import { query, param, validationResult } from 'express-validator';
import { authenticate } from '../middleware/auth.middleware';

const router = Router();

router.get(
  '/',
  [
    query('page').optional().isInt({ min: 1 }).toInt(),
    query('limit').optional().isInt({ min: 1, max: 100 }).toInt(),
  ],
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });

    const page = req.query.page ?? 1;
    const limit = req.query.limit ?? 10;
    // 取得分頁資料...
    res.json({ page, limit, data: [] });
  }
);

4️⃣ 資料庫整合:Prisma(或 TypeORM)

4.1 Prisma 初始化

npm install prisma @prisma/client
npx prisma init

prisma/schema.prisma(已示範 User),再加上簡單的 Post 模型:

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
}

執行遷移:

npx prisma migrate dev --name init

4.2 CRUD Service 範例

// src/services/post.service.ts
import { PrismaClient, Post } from '@prisma/client';
const prisma = new PrismaClient();

export async function createPost(data: { title: string; content?: string; authorId: number }): Promise<Post> {
  return prisma.post.create({ data });
}

export async function getPosts(skip = 0, take = 10) {
  return prisma.post.findMany({ skip, take, include: { author: true } });
}

export async function getPostById(id: number) {
  return prisma.post.findUnique({ where: { id }, include: { author: true } });
}

export async function updatePost(id: number, data: Partial<Post>) {
  return prisma.post.update({ where: { id }, data });
}

export async function deletePost(id: number) {
  return prisma.post.delete({ where: { id } });
}

4.3 結合路由與授權

// src/routes/post.routes.ts
import { Router } from 'express';
import {
  createPost,
  getPosts,
  getPostById,
  updatePost,
  deletePost,
} from '../services/post.service';
import { authenticate, authorize } from '../middleware/auth.middleware';
import { body, param, validationResult } from 'express-validator';

const router = Router();

router.use(authenticate); // 所有路由皆需驗證

router.post(
  '/',
  [
    body('title').isString().notEmpty(),
    body('content').optional().isString(),
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });

    const { title, content } = req.body;
    const authorId = req.user!.sub; // 由 JWT 取得
    const post = await createPost({ title, content, authorId });
    res.status(201).json(post);
  }
);

router.get('/', async (req, res) => {
  const page = Number(req.query.page) || 1;
  const limit = Number(req.query.limit) || 10;
  const posts = await getPosts((page - 1) * limit, limit);
  res.json(posts);
});

router.put(
  '/:id',
  [
    param('id').isInt(),
    body('title').optional().isString(),
    body('content').optional().isString(),
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });

    const id = Number(req.params.id);
    const post = await getPostById(id);
    if (!post) return res.status(404).json({ message: 'Post not found' });

    // 只允許作者或 admin 編輯
    if (post.authorId !== req.user!.sub && req.user!.role !== 'admin') {
      return res.status(403).json({ message: '沒有權限' });
    }

    const updated = await updatePost(id, req.body);
    res.json(updated);
  }
);

router.delete(
  '/:id',
  [param('id').isInt()],
  async (req, res) => {
    const id = Number(req.params.id);
    const post = await getPostById(id);
    if (!post) return res.status(404).json({ message: 'Post not found' });

    if (post.authorId !== req.user!.sub && req.user!.role !== 'admin') {
      return res.status(403).json({ message: '沒有權限' });
    }

    await deletePost(id);
    res.status(204).send();
  }
);

export default router;

server.ts 中掛載:

import postRoutes from './routes/post.routes';
app.use('/api/posts', postRoutes);

常見陷阱與最佳實踐

陷阱 說明 解決方式
JWT 密鑰硬編碼 secret 寫死在程式碼會導致安全漏洞。 使用 環境變數 (process.env.JWT_SECRET) 並在部署時設定。
忘記在 token 中設定過期時間 永久有效的 token 會增加被盜用的風險。 設定 expiresIn(如 7d)或使用 refresh token 機制。
驗證錯誤未回傳 只回傳 400,卻沒有錯誤訊息,除錯困難。 統一 錯誤格式(如 { errors: [...] }),前端可直接呈現。
資料庫查詢未加索引 大量 findMany 時效能急遽下降。 依照常用搜尋條件在 Prisma schema 中加入 @@index([...])
未使用 Transaction 多表寫入失敗時會留下不一致資料。 Prisma 提供 prisma.$transaction([...]),或 TypeORM 的 queryRunner
過度信任 client 輸入 直接把 req.body 寫入 DB 會產生 SQL Injection。 驗證 + 轉型(class‑validator)後再使用 ORM。

最佳實踐

  1. 統一錯誤處理:在 src/middleware/error.middleware.ts 中捕捉所有未處理的例外,回傳標準 JSON。
  2. 日志與監控:使用 winstonpino 記錄請求與錯誤,配合 ELKGrafana 監控。
  3. 測試驅動:使用 jest + supertest 撰寫單元與整合測試,確保 auth、validation、DB 邏輯不會回歸。
  4. 環境分離:開發、測試、正式環境分別使用不同的資料庫與 JWT 秘鑰。

實際應用場景

場景 為什麼需要此套件/概念 範例實作
社群平台 使用者需 註冊 / 登入,發文只能由已驗證帳號執行。 前述 auth.routes.ts + post.routes.ts 結合 authorize(['admin']) 讓管理員刪除不當貼文。
電商系統 商品 CRUD 必須驗證 管理員權限,同時防止 價格被惡意修改 product.routes.ts 中加入 authorize(['admin', 'manager']),並使用 class-validator 驗證價格為正整數。
多租戶 SaaS 每個租戶只能存取自己資料庫資料。 JWT 中加入 tenantId,在 Prisma query 前加上 where: { tenantId: req.user!.tenantId }
行動 App 後端 手機端常使用 短期 token(存於 Secure Storage),伺服器需快速驗證。 使用 express-jwt 或自訂 authenticate 中介層,配合 CORSHelmet 增強安全。
資料分析平台 大量查詢需要 分頁排序,同時避免 SQL 注入 express-validator 驗證 sortorder 參數,Prisma orderBy 動態組合。

總結

本文從 零件化 的角度,示範了在 Express + TypeScript 中加入 認證 (JWT)參數驗證 以及 資料庫 (Prisma) 的完整流程。透過以下步驟即可打造一個 安全、可維護、可擴充 的 API 服務:

  1. 初始化 TypeScript + Express,配置基礎中介層。
  2. 實作 JWT 認證,並以中介層統一擷取使用者資訊。
  3. 使用 class‑validator / express‑validator,在 DTO 或 query 層面完成驗證。
  4. 以 Prisma 為 ORM,定義模型、執行遷移,並撰寫 CRUD Service。
  5. 結合授權、錯誤處理、日誌,提升系統穩定性與可觀測性。

掌握以上核心概念後,你就能在 實戰專案 中快速擴增功能(如 OAuth、Refresh Token、Rate Limiting),並以 測試驅動 的方式持續優化。祝你開發順利,打造出高品質的 API 服務! 🚀