首页 / Prisma ORM 入门教程 / Express 与 NestJS 集成

Prisma ORM 入门教程

Express 与 NestJS 集成

本教程共 54 篇 · 第 47 篇 · 更新于 2026-08-11 · 约 6 分钟阅读

ExpressNestJSREST API依赖注入ZodPrismaService异常过滤器

本节目标:学会在 Express 和 NestJS 两个最常用的 Node.js 框架里接入 Prisma,写出类型安全、错误处理规范的 CRUD API。

Express:最小 CRUD 全流程

Express 是 Node.js 生态最基础的 Web 框架。接入 Prisma 只需要三样东西:驱动程序适配器(Driver Adapter,简称驱动适配器)、Prisma Client 单例、路由处理器。

先安装依赖。v7 强制使用驱动程序适配器连接数据库:

npm install express @prisma/client @prisma/adapter-pg pg
npm install --save-dev prisma typescript tsx @types/express

创建单例文件。每个进程只实例化一个 PrismaClient,避免每次请求都新建连接池:

// src/lib/prisma.ts
import { PrismaClient } from "../generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

const adapter = new PrismaPg({
  connectionString: process.env.DATABASE_URL,
});

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    adapter,
    log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
  });

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
Note

导入路径是生成器 output 指定的自定义目录(如 src/generated/prisma),不是 @prisma/client。url 配置放在 prisma.config.ts,用 env("DATABASE_URL") 读取。

路由里写查询

查询写在路由处理器里,直接调用 prisma 模型方法:

app.get("/posts/:id", async (req, res) => {
  const post = await prisma.post.findUnique({
    where: { id: Number(req.params.id) },
    include: { author: { select: { name: true } } },
  });
  if (!post) return res.status(404).json({ error: "Not found" });
  res.json(post);
});

app.post("/posts", async (req, res) => {
  const post = await prisma.post.create({
    data: {
      title: req.body.title,
      author: { connect: { email: req.body.authorEmail } },
    },
  });
  res.status(201).json(post);
});

两个细节值得记住:include 里嵌套 select 只取需要的关联字段,避免把整个 author 记录发给前端;创建关联记录用 connect,一条语句完成外键写入。

Zod 校验:编译期类型不等于运行时安全

Prisma 生成的是编译期类型。请求体来自网络,可能是任意内容,必须做运行时校验。Zod 是最常用的方案:

import { z } from "zod";

const CreatePost = z.object({
  title: z.string().min(3).max(200),
  slug: z.string().regex(/^[a-z0-9-]+$/),
  authorEmail: z.string().email(),
  published: z.boolean().default(false),
});

app.post("/posts", async (req, res) => {
  const parsed = CreatePost.safeParse(req.body);
  if (!parsed.success) return res.status(400).json(parsed.error.format());
  const post = await prisma.post.create({ data: parsed.data });
  res.status(201).json(post);
});

safeParse 不会抛异常,校验失败返回 400,成功的数据类型已经和 Prisma 输入类型对齐。

REST 语义映射

Prisma 错误码要翻译成正确的 HTTP 状态码,不能一律 500。最常见两组映射:

Prisma 错误码含义HTTP 状态码
P2002唯一约束冲突409 Conflict
P2025记录不存在404 Not Found
app.put("/users/:id", async (req, res) => {
  try {
    const user = await prisma.user.update({
      where: { id: Number(req.params.id) },
      data: { name: req.body.name },
    });
    res.json(user);
  } catch (e) {
    if (e.code === "P2025") return res.status(404).json({ error: "User not found" });
    if (e.code === "P2002") return res.status(409).json({ error: "Email already exists" });
    throw e;
  }
});

单文件跑通后,建议按职责拆文件:lib/prisma.ts 放单例,routes/ 放路由,schemas/ 放 Zod 定义。路由里只做参数解析、校验和响应格式化,业务逻辑抽到 service 层。Prisma 查询集中在一个数据访问层,错误处理也统一,接口多了不会乱。

NestJS:依赖注入模式

NestJS 的思路是把数据库连接封装成可注入的服务。先建 PrismaService,继承 PrismaClient 并实现生命周期钩子:

// src/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from "@nestjs/common";
import { PrismaClient } from "./generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  constructor() {
    const adapter = new PrismaPg({
      connectionString: process.env.DATABASE_URL,
    });
    super({ adapter });
  }

  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}
Note

onModuleInit 启动时主动连接,把连接错误暴露在启动阶段而不是第一个请求里;onModuleDestroy 负责优雅关闭连接池。官方指南已不再使用 enableShutdownHooks + beforeExit 组合,推荐生命周期钩子写法(onModuleInit/onModuleDestroy 显式管理连接);原生 beforeExit 钩子仍可用,见第 39 章。

在 main.ts 调用 app.enableShutdownHooks(),Docker 收到 SIGTERM 时才会触发销毁钩子。

PrismaModule:全局模块

把 PrismaService 放进模块并声明为全局,任何业务模块都能直接注入:

import { Module, Global } from "@nestjs/common";
import { PrismaService } from "./prisma.service";

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

业务 Service 构造函数注入即可:

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  findAll() {
    return this.prisma.user.findMany();
  }
}

两个高频坑:PrismaService 声明了但不 exports,注入直接报错;在多个 Service 里各自 new PrismaClient(),会耗尽连接池。

常见集成错误

三件事最常踩坑:

  1. 忘记设 "type": "module"。v7 是 ESM-only,写 require 直接报错。
  2. 在多个文件里各自 new PrismaClient()。每个实例都有自己的连接池,连接数翻倍增长,迟早耗尽。
  3. 改了 Schema 忘了 prisma generate。类型停留在旧版本,编译期错误和运行时错误都变得莫名其妙。

异常过滤器统一错误

NestJS 里每个控制器都 try/catch 太啰嗦。用全局异常过滤器集中处理 Prisma 错误:

import { Catch, ExceptionFilter, ArgumentsHost, HttpStatus } from "@nestjs/common";
import { Prisma } from "./generated/prisma/client";
import { Response } from "express";

@Catch(Prisma.PrismaClientKnownRequestError)
export class PrismaExceptionFilter implements ExceptionFilter {
  catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) {
    const response = host.switchToHttp().getResponse<Response>();
    if (exception.code === "P2002")
      return response.status(HttpStatus.CONFLICT).json({ message: "记录已存在" });
    if (exception.code === "P2025")
      return response.status(HttpStatus.NOT_FOUND).json({ message: "记录不存在" });
    return response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ message: "数据库错误" });
  }
}

在 main.ts 里 app.useGlobalFilters(new PrismaExceptionFilter()) 注册一次,全 API 的 Prisma 错误都变成语义化的状态码。

参考来源

  • Prisma 官方文档:NestJS 集成指南(guides/frameworks/nestjs.mdx)
  • Tech Insider:Build a Type-Safe API in 13 Steps(2026)
  • Generalist Programmer:NestJS Prisma Tutorial: Complete Guide