Express 与 NestJS 集成
本教程共 54 篇 · 第 47 篇 · 更新于 2026-08-11 · 约 6 分钟阅读
本节目标:学会在 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(),会耗尽连接池。
常见集成错误
三件事最常踩坑:
- 忘记设
"type": "module"。v7 是 ESM-only,写require直接报错。 - 在多个文件里各自
new PrismaClient()。每个实例都有自己的连接池,连接数翻倍增长,迟早耗尽。 - 改了 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