在Node.js环境中利用Prisma和Nexus构建类型安全的GraphQL API。
按照七步流程构建Node.js GraphQL API,将该API的Prisma数据模型与Nexus生成的类型及解析器相结合。
了解如何将 Prisma Nexus 引入 Node.js 项目以生成类型安全的 GraphQL API,内容包括模式设计、解析器逻辑以及运行中的服务器。
想象这样一个 GraphQL 项目:同一个“User”类型被定义在四个不同的地方——SDL 文档、手写的 TypeScript 接口、Prisma 模型,以及团队成员在项目发布数月后添加的 Zod 验证器。每当其中某个定义发生变化时,至少会有另一个定义出现不同步。修复版本发布后,TypeScript 类型仍然认为phone字段是必填的,尽管该字段早在几周前就已经从数据库中移除了。
这种混乱正是将 Prisma 与 Nexus 配合使用所要避免的。Nexus 直接从你在 Prisma 中已定义的数据模型生成 GraphQL 模式和 TypeScript 类型。只有一个真实的数据来源,所有后续内容都由此衍生而来。只需更新一次定义,类型、模式以及解析器签名就会同步变化。说出来似乎是常识——真正的收获来自于在没有它的情况下工作,体会那种缺失带来的高昂代价。
本指南将带领你使用 Prisma 和 Nexus 从零开始构建一个 Node.js GraphQL API,共分为七个步骤,每一步都提供完整代码且不遗漏任何内容。完成后,你将拥有一个与 PostgreSQL 相连的可运行服务器——你可以放心地运行、扩展并进一步优化它。该指南旨在为真正的生产级电商后端提供足够稳固的基础,而非那种添加第二个模型就会出问题的演示版本。
在开始第一步之前需要准备什么
你需要:
- 已安装的 Node.js——如果尚未安装,请从 nodejs.org 下载当前 LTS 版本。
- 全局可用的 Prisma CLI:
npm install -g prisma
- 一个可访问的正在运行的 PostgreSQL 数据库。可以是本地的 Docker 容器、Supabase 的免费套餐,或是 Railway 等托管服务——只要你有对应的连接字符串即可。
对于那些打算将此方案应用于现有代码库而非全新项目的人,请注意:在首次迁移时,Prisma会尝试将schema.prisma与数据库中已有的结构进行协调。如果原有架构较为混乱,这一协调过程可能会生成大量令人望而生畏的差异内容。在应用这些更改之前,请仔细阅读差异报告,并务必先在开发环境中进行测试。如果您是从零开始,那么这些注意事项暂时不适用于您。
步骤1:启动项目
这是整个流程中最快的步骤。只需创建一个文件夹,然后一次性拉取所有依赖项即可:
mkdir prisma-nexus-graphql
cd prisma-nexus-graphql
# Initialize your project
npm init -y# Install required dependencies
npm install graphql nexus prisma express apollo-server-express path
这条命令会一次性加载全部七个包:GraphQL运行时、用于代码优先模式构建的Nexus、Prisma本身,以及用于运行服务器的Apollo/Express组合。将所有包一起安装不仅方便,还能让npm一次性解析整个集合中的相互依赖关系,而不会像逐个安装包那样出现次要版本不匹配的风险。
步骤2:将Prisma连接到数据库
npx prisma init
按照提示操作并选择PostgreSQL。命令执行完成后,会出现两个之前不存在的新文件:
prisma/schema.prisma—— 这里存放你的数据模型.env—— 这里用于存放DATABASE_URL连接字符串,应立即将其放入此处
这并非夸张。在接触模式定义之前、在执行迁移之前、甚至在做任何其他操作之前,先将连接字符串放入 .env 文件中。从这时起,几乎所有的 Prisma 命令都会尝试连接到数据库,而当连接字符串缺失或格式错误时出现的错误信息往往毫无用处。你不会看到清晰的“无效连接字符串”提示,反而会得到关于客户端未初始化的模糊错误信息——这样你就很容易浪费十五分钟去排查错误的根源。
步骤 3:编写 Prisma 模式定义——这与 GraphQL 模式不同
如果你之前使用过 GraphQL,但从未与 Prisma 结合使用过,请不要将 schema.prisma 视为设计 API 接口的地方。它并非如此,而只是数据库结构的表示——包括表、列、关联关系以及约束条件。真正的 API 结构是通过 Nexus 在之后从这些信息中派生出来的。请牢记这一区别,因为它有助于保持整体思维模型的连贯性。
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}model User {
id Int @id @default(autoincrement())
name String
email String @unique
}
一旦模型编写完成,就运行迁移命令:
npx prisma migrate dev
这条命令只负责两件事,其他步骤则不做任何操作:它在数据库中创建实际的表,并使用与当前架构完全匹配的 TypeScript 类型重新生成 Prisma Client。如果跳过这一步,Prisma Client 就无法识别 User 模型的存在。这样一来,错误信息会隐藏在您无法控制的生成文件中,调用堆栈也毫无用处——根本没有巧妙的捷径可走。无论何时架构发生变化,都必须运行迁移操作。
第 4 步:Nexus —— 为何多一个文件也值得
在完成这些设置后,我们不禁要问:Nexus 真的发挥了应有的作用吗?其实不使用它也能构建 GraphQL 服务器——可以手动编写 SDL,自行定义 TypeScript 接口,并将所有内容手动连接到解析器上。很多代码库都是这么做的。但问题在于,这种方式容易引发某种特定的错误:SDL 所描述的形状与 TypeScript 类型所定义的略有不同,而解析器返回的结果则完全另当别论。要弄清楚这三个版本中哪个才是“真实”的,往往需要花费的时间比最初实现该功能还要多。
Nexus 通过将 SDL 视为生成结果而非手动编写的代码来避免该问题。你在 TypeScript 中定义类型,Nexus 便会从这一单一来源同时推导出 SDL 及相应的类型定义。原本可能相互脱节的三个部分现在合为一体,从结构上就不可能出现矛盾。以下是 schema.ts 的内容:
// schema.ts
import { makeSchema } from 'nexus';
import path from 'path';
import * as resolvers from './resolvers';const schema = makeSchema({
types: [resolvers],
outputs: {
schema: path.join(__dirname, './generated/schema.graphql'),
typegen: path.join(__dirname, './generated/nexus.ts'),
},
});export default schema;
outputs配置决定了Nexus应将生成的文件保存到何处:generated/schema.graphql用于存储SDL格式的文件,而generated/nexus.ts则存放对应的TypeScript定义文件。这两类文件在每次运行时都会被重新生成,因此绝不可手动修改它们。如果打开generated/nexus.ts后发现需要修正的内容,切勿直接编辑该文件——应找到对应的源定义并在那里进行修改。修改生成的文件就如同修补已编译的二进制文件:这样的改动在下次构建时会被悄悄覆盖。
第5步:解析器——将模式与数据库连接起来
// resolvers.ts
import { extendType, stringArg, nonNull, objectType } from 'nexus';
import { PrismaClient } from '@prisma/client';const prisma = new PrismaClient();export const User = objectType({
name: 'User',
definition(t) {
t.nonNull.id('id')
t.string('name')
t.string('email')
},
})export const Query = extendType({
type: 'Query',
definition(t) {
t.list.field('users', {
type: 'User',
resolve: async () => {
return await prisma.user.findMany();
},
});
},
});export const Mutation = extendType({
type: 'Mutation',
definition(t) {
t.field('createUser', {
type: 'User',
args: {
name: nonNull(stringArg()),
email: nonNull(stringArg()),
},
resolve: async (_, args) => {
return await prisma.user.create({
data: {
name: args.name,
email: args.email,
},
});
},
});
},
});
请注意,PrismaClient仅在模块的最顶层、任何函数体之外被实例化一次。这一位置的选择比乍看之下更为重要。每次调用new PrismaClient()都会建立一条新的数据库连接。如果将其放在解析器内部创建,那么每次请求都会触发新的连接建立。在正常的本地开发环境中,每秒可能只有一到两次请求,数据库甚至察觉不到差异。但在真正的并发流量下——比如促销期间有数百名顾客同时访问/checkout——这种模式会耗尽 PostgreSQL 的连接限制,导致在高负载情况下出现错误。
在模块级别声明客户端意味着整个进程共享一个连接。各个请求不会争相打开自己的数据库连接,而是排入同一个共享客户端队列中,该客户端会内部管理自身的连接池。这类细节是经验丰富的 Node.js 开发者会本能地处理的,而经验较少的团队则往往要在出问题时才艰难地意识到这一点。现在你就可以避开这个教训了。
第6步:服务器
// server.ts
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import schema from './schema';const app = express();
const server = new ApolloServer({ schema });const startServer = async () => {
await server.start(); // Start Apollo Server server.applyMiddleware({ app }); // Apply Apollo Server middleware to Express const PORT = process.env.PORT || 4000; app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}/graphql`);
});
}startServer().catch((err) => {
console.error('Error starting the server:', err);
});
在开始使用之前有一个需要注意的细节:await server.start()必须先于server.applyMiddleware()执行。Apollo Server 2中并不存在这样的顺序要求——Apollo 3引入了明确的异步启动阶段,而2021年底之前编写的任何示例代码很可能完全缺少这一调用。如果跳过这一步,就会出现Server must be started before calling server.applyMiddleware错误,虽然它没有解释为何有此规则,但至少能说明问题所在。一旦理解了背后的原因,只需两秒钟就能解决,无需费力绕弯。
第7步:启动它。破坏它。信任它。
node server.ts
访问http://localhost:4000/graphql,即可进入GraphQL Playground。首先运行变异操作:
// Fetch Users
query {
users {
id
name
email
}
}
// Create Users
mutation {
createUser(name: "John Doe", email: "john@example.com") {
id
name
email
}
}
在执行查询之前先运行变异操作,这样才有数据可获取。注意查看查询响应中返回的你刚刚插入的记录。接着做大多数教程都会跳过的步骤:打开数据库客户端——无论是 psql、TablePlus、DBeaver 还是其他工具——直接查看 User 表,而不是 API 返回的 JSON 数据,而是表本身的原始内容。
你的那行数据就在那里,它是由你在 TypeScript 中使用 Nexus 类型定义的 GraphQL 变异操作生成的,通过 Prisma 执行,并最终存储在 PostgreSQL 中。这条链路上的每一个环节都正常工作。你可以精确地找到你的应用程序代码与数据库交互的位置。对于那些长期使用 REST 接口和手动编写 SQL 的人来说,通常在这个时刻,这套技术栈才不再像一张示意图,而真正变得有实际意义。
你已经构建了什么以及还需要添加什么
你现在拥有的是一个可用的后端基础,而非简单的示例。你刚刚遵循的流程——定义 Prisma 模型、执行迁移、添加 Nexus objectType、编写解析器并将其集成到 Apollo/Express 服务器中——正是你在引入每个新模型时都需要重复的操作。无论是 Product、Order 还是 Cart,步骤都不会改变,其可靠性也同样有保障。在 schema.prisma 中添加关联关系,运行 migrate dev,然后实现解析器,类型就会自动更新。这种自动同步正是该架构的核心优势——你不再需要依赖内存来保持模式、类型和解析器的一致性,因为工具会为你强制执行这一点。
目前明显缺失的是身份验证、权限控制、速率限制以及输入校验功能。Nexus仅能确保类型正确,却无法规定谁有权限调用哪些操作。目前情况下,只要能够访问4000端口,任何人都可以成功触发createUser操作。在本地开发时这尚可接受,但一旦该API可以通过真实网址访问,这就不再合适了。在将其部署到他人可访问的环境之前,必须先添加身份验证中间件。
如需更深入的了解,可查看 Prisma文档中关于关联、过滤和分页的内容,以及 Nexus文档中关于字段级授权和自定义标量的内容。这两套文档的结构都非常清晰,便于从头到尾通读,而不仅仅是在出现问题时匆匆浏览解决方案——这种特性在技术文档中实属难得。
相关阅读
- 利用LangChain Guardrails和中间件构建安全的AI智能体 — 了解LangChain中基于模型的确定性防护机制如何用于检测个人信息泄露、执行业务规则,以及为AI智能体添加人工审批流程。