本文 AI 產出,尚未審核

ExpressJS (TypeScript) 與 Prisma 整合

讓 TypeScript 與資料庫的互動變得安全、直覺且高效


簡介

在現代的 Node.js 生態系中,Express 仍是最受歡迎的 Web 框架,而 TypeScript 則為 JavaScript 帶來了靜態型別的保障。兩者結合後,開發者可以在寫 API 時即時得到編譯期的錯誤提示,極大提升開發效率與程式碼品質。

然而,僅有 Express 與 TypeScript 還不足以完整構築一套可靠的後端系統,資料庫 的存取是不可或缺的一環。傳統的 ORM(如 Sequelize、TypeORM)在型別推斷與開發體驗上常常會出現「型別不匹配」或「手動寫 SQL」的痛點。

Prisma 以「型別安全的資料庫工具」自居,與 TypeScript 天生契合。它的 schema‑first 設計、生成的 Prisma Client 以及自動化的 migration 機制,使得在 Express 中操作資料庫變得既簡潔又可靠。本文將從概念說明到實作範例,帶你一步步完成 Express + TypeScript + Prisma 的完整整合。


核心概念

1. Prisma 是什麼?

Prisma 是一套 下一代 ORM,核心由三個部分組成:

組件 功能 為什麼重要
Prisma Schema .prisma 檔描述資料模型、資料庫連線與關聯 單一來源的模型定義,避免程式碼與資料庫不同步
Prisma Migrate 依照 schema 自動產生 migration,管理資料庫變更 減少手動撰寫 SQL,確保每次部署都有可追溯的變更紀錄
Prisma Client 依 schema 產生的 TypeScript 客戶端,提供型別安全的 CRUD API 編譯期即能捕捉欄位名稱、關聯錯誤,提升開發體驗

2. 為什麼 Prisma 與 TypeScript 搭配是「最佳」?

  • 型別安全:Prisma Client 會根據 schema 產生對應的 TypeScript 型別,IDE 能即時提示欄位、關聯、可選屬性等資訊。
  • 自動補全:在 VS Code 中使用 prisma.user.findUnique 時,會自動列出 where, select, include 等可用參數。
  • 可預測的執行結果:所有資料庫操作皆回傳明確的型別,減少 any 帶來的執行期錯誤。

3. Prisma Schema 基礎

// prisma/schema.prisma
datasource db {
  provider = "postgresql"          // 支援 PostgreSQL、MySQL、SQLite、SQL Server
  url      = env("DATABASE_URL")  // 透過 .env 管理機密資訊
}

generator client {
  provider = "prisma-client-js"
  output   = "../src/generated/prisma-client"
}

// 資料模型範例
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
}

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

重點@relation@unique@default 等屬性會直接映射到 Prisma Client 的型別定義,讓後續程式碼自動得到正確的型別資訊。

4. 產生 Prisma Client

# 安裝 Prisma CLI(開發依賴)
npm install -D prisma

# 初始化 Prisma(會產生 schema.prisma)
npx prisma init

# 產生 Client(會在 src/generated/... 產生 .ts 檔)
npx prisma generate

產生的 prisma-client 只需要在程式碼中 import 即可使用:

import { PrismaClient } from '../generated/prisma-client';
export const prisma = new PrismaClient();

5. 整合到 Express

在 Express 中,我們通常會把 Prisma Client 放在 middlewareservice 層,讓每一次請求都可以共享同一個實例(或依需求使用 per‑request instance)。

// src/app.ts
import express from 'express';
import { prisma } from './prisma';

const app = express();
app.use(express.json());

// 讓 Prisma 在每個 request 後自動斷開連線(防止連線泄漏)
app.use(async (req, res, next) => {
  res.on('finish', async () => {
    await prisma.$disconnect();
  });
  next();
});

程式碼範例

以下示範 5 個在 Express + TypeScript 中常見的 Prisma 用法,皆附有說明註解。

範例 1️⃣:建立新使用者(Create)

// src/routes/user.ts
import { Router, Request, Response } from 'express';
import { prisma } from '../prisma';

const router = Router();

router.post('/users', async (req: Request, res: Response) => {
  const { email, name } = req.body;

  try {
    // Prisma Client 會自動根據 schema 推斷欄位型別
    const user = await prisma.user.create({
      data: {
        email,
        name,          // name 為 optional,若未提供會被設定為 null
      },
    });
    res.status(201).json(user);
  } catch (error) {
    // 例如 email 重複時會拋出 PrismaClientKnownRequestError
    res.status(400).json({ error: (error as Error).message });
  }
});

export default router;

範例 2️⃣:取得使用者與其文章(Read + Include)

router.get('/users/:id', async (req: Request, res: Response) => {
  const userId = Number(req.params.id);

  const user = await prisma.user.findUnique({
    where: { id: userId },
    include: {                // 直接把關聯的 posts 包進回傳結果
      posts: {
        where: { published: true },
        select: { title: true, createdAt: true },
      },
    },
  });

  if (!user) return res.status(404).json({ error: 'User not found' });
  res.json(user);
});

範例 3️⃣:更新文章(Update)與型別安全

router.patch('/posts/:id', async (req: Request, res: Response) => {
  const postId = Number(req.params.id);
  const { title, content, published } = req.body;

  const updated = await prisma.post.update({
    where: { id: postId },
    data: {
      title,
      content,
      published,
    },
  });

  res.json(updated);
});

提示:若 published 欄位忘記傳值,TypeScript 仍會允許 undefined,但 Prisma 只會更新實際提供的欄位。

範例 4️⃣:刪除使用者(Delete)並處理關聯

router.delete('/users/:id', async (req: Request, res: Response) => {
  const userId = Number(req.params.id);

  // 先刪除所有文章,避免外鍵衝突
  await prisma.post.deleteMany({ where: { authorId: userId } });

  const deleted = await prisma.user.delete({
    where: { id: userId },
  });

  res.json({ message: 'User removed', deleted });
});

範例 5️⃣:Transaction(原子操作)

router.post('/posts/:id/publish', async (req: Request, res: Response) => {
  const postId = Number(req.params.id);

  const result = await prisma.$transaction(async (tx) => {
    // 1. 把文章設為已發布
    const post = await tx.post.update({
      where: { id: postId },
      data: { published: true },
    });

    // 2. 同時更新作者的最後發布時間
    await tx.user.update({
      where: { id: post.authorId },
      data: { updatedAt: new Date() },
    });

    return post;
  });

  res.json(result);
});

重點$transaction 內的所有操作要麼全部成功,要麼全部回滾,確保資料一致性。


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記執行 prisma generate 修改 schema 後未重新產生 client,導致型別不匹配或 runtime error。 每次修改 .prisma 檔後執行 npx prisma generate,或在 package.json 加入 postinstall script。
資料庫連線泄漏 在開發環境未妥善關閉 Prisma 連線,導致 ECONNREFUSED 在 Express 結束回應時呼叫 prisma.$disconnect(),或在應用關閉時使用 process.on('SIGTERM')
過度使用 any 為了省事直接把 Prisma 回傳值 cast 為 any,失去型別安全。 盡量保留 Prisma 自動生成的型別,例如 UserPost,必要時使用 Pick<>Omit<> 產生子型別。
忽視 migration 歷史 手動改資料表卻未同步到 Prisma schema,造成生產環境不一致。 使用 prisma migrate dev 產生 migration,並在 CI/CD 流程中執行 prisma migrate deploy
未處理 Prisma 錯誤類別 Prisma 拋出多種錯誤(PrismaClientKnownRequestErrorPrismaClientValidationError),未分類處理會讓 API 回傳不一致。 建立全域錯誤處理 middleware,根據 error.code 返回相應的 HTTP status。

最佳實踐

  1. 統一管理 Prisma 實例:在 src/prisma.ts 中建立單例,避免在每個模組重複 new PrismaClient()
  2. 使用環境變數管理資料庫連線.env 檔內只放 DATABASE_URL,別把密碼寫在程式碼裡。
  3. 利用 select / include 精確控制返回欄位,減少不必要的資料傳輸與 N+1 問題。
  4. 在測試環境使用 SQLite 或獨立的測試資料庫,搭配 prisma migrate reset 快速重置測試資料。
  5. 設定 Prisma Loggingprisma.$on('query', (e) => console.log(e)); 方便除錯與效能監控。

實際應用場景

1. 電子商務平台的訂單系統

  • 需求:訂單、商品、使用者三個模型互相關聯,必須保證「下單」與「庫存扣減」的原子性。
  • 使用 Prisma:透過 @relation 建立外鍵,使用 $transaction 同時寫入 OrderOrderItem 並更新 Product.stock
  • 好處:型別安全保證每筆資料都有正確的欄位,若庫存不足會在 transaction 內拋錯並自動回滾,避免產生錯誤的訂單紀錄。

2. 多租戶 SaaS 系統

  • 需求:每個租戶擁有獨立的資料表或 schema,且需要在 API 中根據 JWT 中的 tenantId 動態切換資料庫。
  • 使用 Prisma:在 prisma 實例化時傳入不同的 datasource URL(透過 prisma.$connect()),配合 Express 中間件注入 tenantId
  • 好處:Prisma 的 type‑safe client 仍然適用於每個租戶,且 migration 可以一次性部署到所有租戶的資料庫。

3. 即時聊天系統(WebSocket + REST)

  • 需求:訊息需要儲存、查詢最近聊天紀錄、支援分頁。
  • 使用 PrismaMessage 模型的 createdAt 欄位自動生成索引,使用 findMany 搭配 orderByskiptake 完成分頁;在 WebSocket 中使用同一 Prisma 實例寫入訊息,確保資料一致。
  • 好處:一次編寫的 TypeScript 型別在 REST 與 WebSocket 兩端皆可共享,減少重複定義與錯誤。

總結

  • Prisma 為 TypeScript 提供了 型別安全、開發友好 的資料庫存取層,與 Express 完美結合後,開發者可以在撰寫 API 時同時享受到編譯期錯誤檢查與自動補全的便利。
  • 透過 Prisma Schema 定義模型、Prisma Migrate 管理 migration、Prisma Client 產生型別安全的 CRUD 方法,我們可以在 Express 中以最少的程式碼完成資料庫操作,同時保持代碼的可讀性與可維護性。
  • 本文提供的 5 個實作範例、常見陷阱與最佳實踐,已足以讓你在專案中快速上手。未來若需要擴充功能(多租戶、交易、效能監控),Prisma 仍提供靈活的 API 與豐富的社群資源,值得在任何 Node.js/TypeScript 專案中長期使用。

下一步:把上述範例整合到你的專案,使用 npm run dev 觀察 Prisma 與 Express 的即時互動,並在 CI 流程中加入 prisma migrate deploy,讓資料庫變更永遠保持可追蹤、可回溯。祝開發順利,寫出安全、乾淨的後端程式碼!