HowToGraphQL 之 TypeScript + Apollo Server 全栈 GraphQL 服务实战总结:从零搭建到生产部署

📅 发布时间:2026/9/25 2:53:30
HowToGraphQL 之 TypeScript + Apollo Server 全栈 GraphQL 服务实战总结:从零搭建到生产部署
【免费下载链接】howtographqlThe Fullstack Tutorial for GraphQL项目地址https://gitcode.com/gh_mirrors/ho/howtographql点击查看免费下载本篇是 HowToGraphQL 仓库中 TypeScript Apollo 系列教程 的收官总结系统回顾以 TypeScript、Apollo Server、Nexus 与 Prisma 为核心技术栈从零构建 Hacker News 克隆版 GraphQL 服务端 API 的完整技术路线并归纳查询/变更解析、数据库接入、JWT 认证、多对多关系、自定义标量以及过滤/分页/排序等关键能力帮助读者建立GraphQL 服务端工程化的整体认知。一、本系列教程的最终技术栈与总体目标本教程的目标是使用以下技术组合从零搭建一个可投入生产实践的 GraphQL 服务端TypeScriptJavaScript 的强类型超集提供类型安全与更优的开发体验Apollo Server功能完整的 GraphQL 服务端库内置基于 Web 的 GraphQL 客户端Apollo Studio Explorer支持 GraphQL 规范、查询性能追踪并可部署到 Vercel、标准 VM、AWS Lambda、Heroku 等环境Nexus采用 code-first 方式构建类型安全的 GraphQL schema 的库——你直接编写 TypeScript 代码来定义 schema由 Nexus 自动生成 GraphQL SDLschema.graphql与 TypeScript 类型定义nexus-typegen.ts从根本上避免 GraphQL 类型与 TypeScript 类型失同步Prisma下一代 Node.js/TypeScript ORM教程中通过Prisma Client在 GraphQL resolver 内部访问数据库。按照 入门章节 的规划整个系列按以下路线推进理解 GraphQL 服务端工作原理用 Nexus 定义 GraphQL schema 并编写对应的 resolver 函数初期仅使用内存数据引入由 Prisma 管理的 SQLite 数据库实现数据持久化实现 signup/login 认证功能并据此做接口权限校验添加 vote 投票功能学习自定义 GraphQL 标量并接入应用为 API 增加排序、过滤与分页能力让客户端可以约束返回列表使用 GitHub Actions 将 API 自动部署到云端Heroku。二、回顾项目搭建从空目录到可运行的 GraphQL 服务2.1 环境与工程初始化教程要求 Node.js 版本为v12及以上可用node --version确认。随后创建项目目录并初始化package.jsonmkdir hackernews-typescript cd hackernews-typescript npm init -y安装 TypeScript 相关工具链与核心依赖npm install --save-dev typescript^4.3.5 ts-node-dev^1.1.8 npm install apollo-server^3.1.1 graphql^15.5.1 nexus^1.1.0ts-node-dev负责在开发时即时转译 TS 文件并在代码变更后自动重启服务tsconfig.json采用strict: true等标准选项rootDir为.、outDir为dist。2.2 Nexus 生成 schema 与两个产物文件在src/schema.ts中通过makeSchema配置 Nexusimport { makeSchema } from nexus import { join } from path export const schema makeSchema({ types: [], outputs: { schema: join(process.cwd(), schema.graphql), typegen: join(process.cwd(), nexus-typegen.ts), }, })运行npx ts-node --transpile-only src/schema后根目录会生成两个文件schema.graphqlGraphQL SDLSchema Definition Language描述 API 的结构nexus-typegen.ts为 schema 中所有类型生成的 TypeScript 类型定义用于保证应用代码的类型安全。随后在package.json中补充两个常用脚本scripts: { dev: ts-node-dev --transpile-only --no-notify --exit-child src/index.ts, generate: ts-node --transpile-only src/schema.ts }2.3 用 Apollo Server 启动服务src/index.ts中创建 ApolloServer 实例并监听端口import { ApolloServer } from apollo-server; import { schema } from ./schema; export const server new ApolloServer({ schema }); const port 3000; server.listen({ port }).then(({ url }) { console.log( Server ready at ${url}); });运行npm run dev后访问http://localhost:3000/即可看到 Apollo Server 欢迎页点击Query your server可进入 Apollo Studio Explorer 在线 GraphQL IDE。若希望使用无需联网的离线 IDE可通过在 ApolloServer 中增加plugins: [ApolloServerPluginLandingPageGraphQLPlayground()]启用 GraphQL Playground。三、Schema 基础三个根类型与查询/变更解析机制3.1 GraphQL schema 与根类型每个 GraphQL schema 都有三个特殊的根类型Query、Mutation和Subscription分别对应 GraphQL 的三种操作类型。根类型上的字段称为根字段它们定义了 API 可用的操作。当根字段的类型是对象类型时查询中需要展开为选择集selection set。教程中的初始 schema 只有默认的ok字段类型为Boolean!!表示非空返回null会触发错误。升级后的 schema 示例展示了列表与非空修饰符的组合语义例如[User!]!表示返回一个本身非空的列表且列表元素也不可为 null。3.2 code-first 工作流使用 Nexus 添加新 API 功能遵循三步流程用 Nexus 定义 schema 组件types、fields、根对象类型等生成 GraphQL SDL 与类型定义为新增字段实现对应的 resolver 函数。定义Link对象类型import { objectType } from nexus; export const Link objectType({ name: Link, definition(t) { t.nonNull.int(id); t.nonNull.string(description); t.nonNull.string(url); }, });所有类型通过src/graphql/index.ts统一导出并作为types传入makeSchemaimport * as types from ./graphql; export const schema makeSchema({ types, outputs: { typegen: join(process.cwd(), nexus-typegen.ts), schema: join(process.cwd(), schema.graphql), }, });3.3 feed 查询与查询解析过程实现feed查询使用extendType扩展Query根类型并定义 resolverlet links: NexusGenObjects[Link][] [ { id: 1, url: www.howtographql.com, description: Fullstack tutorial for GraphQL }, { id: 2, url: graphql.org, description: GraphQL official website }, ]; export const LinkQuery extendType({ type: Query, definition(t) { t.nonNull.list.nonNull.field(feed, { type: Link, resolve(parent, args, context, info) { return links; }, }); }, });查询解析的本质是编排 resolver 调用每个查询字段都对应一个 resolver 函数服务器按查询形状逐层调用并打包响应。每个 resolver 都接收四个参数——parent上一层 resolver 的返回值、args本次操作的参数、context所有 resolver 共享的对象与info。Link类型的id、description、url字段无需显式 resolverGraphQL 类型系统会自动从返回对象的同名属性推断取值这类 resolver 称为 trivial resolver平凡解析器。3.4 post 变更理解 args 参数export const LinkMutation extendType({ type: Mutation, definition(t) { t.nonNull.field(post, { type: Link, args: { description: nonNull(stringArg()), url: nonNull(stringArg()), }, resolve(parent, args, context) { const { description, url } args; let idCount links.length 1; const link { id: idCount, description, url }; links.push(link); return link; }, }); }, });这里第二次用到 resolver 的args参数它携带操作的参数值。由于返回类型被声明为非空Link若将return link改为return nullIDE 会立即报类型错误——这正是 Nexus 类型安全的体现。教程还演示了客户端传参的两种方式内联传值以及通过变量$description、$url分离数据与调用。四、引入数据库Prisma SQLite 与 resolver 的数据库化改造4.1 Prisma 工具集教程使用的 Prisma 生态工具包括Prisma Client自动生成、类型安全的 Node.js/TypeScript 查询构建器Prisma CLI命令行工具用于初始化与交互Prisma Migrate声明式数据建模与迁移系统Prisma Studio可视化的数据查看与编辑 GUI。安装并初始化npm install prisma^3.5.0 --save-dev npm install prisma/client^3.5.0 npx prisma init4.2 定义 Prisma schemaprisma/schema.prisma相当于数据库 schema包含三部分数据源Data source、生成器Generator与数据模型Data modeldatasource db { provider sqlite url file:./dev.db } generator client { provider prisma-client-js } model Link { id Int id default(autoincrement()) createdAt DateTime default(now()) description String url String }id声明主键default(autoincrement())表示自增createdAt的default(now())会在创建记录时自动填充 ISO 8601 格式时间戳。4.3 迁移与 Prisma Client执行npx prisma migrate dev --name init后prisma/migrations目录会生成带.sql文件的迁移历史数据库与 Prisma Client 自动同步。之后可用npx prisma generate独立重新生成 Client。教程用一个独立的src/script.ts验证 Client 的增查能力import { PrismaClient } from prisma/client; const prisma new PrismaClient(); async function main() { const newLink await prisma.link.create({ data: { description: Fullstack tutorial for GraphQL, url: www.howtographql.com, }, }); const allLinks await prisma.link.findMany(); console.log(allLinks); } main() .catch((e) { throw e; }) .finally(async () { await prisma.$disconnect(); });此后更新数据的标准工作流为调整 Prisma 数据模型 → 用prisma migrate迁移数据库 →重新生成 Prisma Client → 在应用代码中使用 Client 访问数据库。4.4 通过 context 把 Prisma Client 注入 resolversrc/context.ts定义 Context 接口并导出 context 对象import { PrismaClient } from prisma/client; export const prisma new PrismaClient(); export interface Context { prisma: PrismaClient; } export const context: Context { prisma };在schema.ts中通过contextType告知 Nexus 上下文类型并在index.ts中把context传给 ApolloServer。随后 resolvers 即可通过第三个参数context.prisma访问数据库export const LinkQuery extendType({ type: Query, definition(t) { t.nonNull.list.nonNull.field(feed, { type: Link, resolve(parent, args, context) { return context.prisma.link.findMany(); }, }); }, });post变更同理改为context.prisma.link.create({ data: { description, url } })。注意 Prisma 查询返回的是 PromiseApollo Server 会自动解析 resolver 返回的 Promise。至此内存数组links与idCount可以彻底删除数据在服务重启后依然持久化。五、认证与授权JWT 方案完整落地5.1 扩展数据模型与关系字段在prisma/schema.prisma中新增User模型并通过relation建立Link与User的一对多关系postedBy/postedById外键model Link { id Int id default(autoincrement()) createdAt DateTime default(now()) description String url String postedBy User? relation(fields: [postedById], references: [id]) postedById Int? } model User { id Int id default(autoincrement()) name String email String unique password String links Link[] }执行npx prisma migrate dev --name add-user-model生成第二次迁移Prisma Client 随之更新。5.2 User/Link 类型与关系 resolverGraphQL 侧新增User类型其links字段需要显式 resolver非平凡 resolver通过parent.id结合 Prisma 的 Fluent API 链式获取关联数据export const User objectType({ name: User, definition(t) { t.nonNull.int(id); t.nonNull.string(name); t.nonNull.string(email); t.nonNull.list.nonNull.field(links, { type: Link, resolve(parent, args, context) { return context.prisma.user .findUnique({ where: { id: parent.id } }) .links(); }, }); }, });Link类型对称地增加可空的postedBy字段resolver 使用findUnique(...).postedBy()。这种链式关系查询即 Prisma 的 Fluent API其批处理行为对解决 GraphQL 中常见的 N1 问题 很有帮助。5.3 signup 与 login 变更安装认证依赖npm install bcryptjs~2.4.0 jsonwebtoken~8.5.0 npm install --save-dev types/bcryptjs~2.4.0 types/jsonwebtoken~8.5.0先定义AuthPayload类型包含token与user再在src/graphql/Auth.ts中实现两个变更export const AuthMutation extendType({ type: Mutation, definition(t) { t.nonNull.field(signup, { type: AuthPayload, args: { email: nonNull(stringArg()), password: nonNull(stringArg()), name: nonNull(stringArg()), }, async resolve(parent, args, context) { const { email, name } args; const password await bcrypt.hash(args.password, 10); const user await context.prisma.user.create({ data: { email, name, password }, }); const token jwt.sign({ userId: user.id }, APP_SECRET); return { token, user }; }, }); }, });login变更则先按email用findUnique查用户再通过bcrypt.compare校验密码成功后签发同样的 JWT。两个 resolver 均为async因为内部需要await多个异步操作。APP_SECRET存放在src/utils/auth.tsexport const APP_SECRET GraphQL-is-aw3some;它用于签名与校验 JWT教程强调该值必须保密生产环境应放入.env等安全位置避免随版本控制泄露。5.4 服务端校验客户端 JWT通过 HTTPAuthorization头传递 Bearer Token。src/utils/auth.ts增加解码函数import * as jwt from jsonwebtoken; export interface AuthTokenPayload { userId: number; } export function decodeAuthHeader(authHeader: String): AuthTokenPayload { const token authHeader.replace(Bearer , ); if (!token) { throw new Error(No token found); } return jwt.verify(token, APP_SECRET) as AuthTokenPayload; }context.ts从对象改为函数形态由 Apollo Server 在收到请求时执行把解码出的userId放入上下文export interface Context { prisma: PrismaClient; userId?: number; } export const context ({ req }: { req: Request }): Context { const token req req.headers.authorization ? decodeAuthHeader(req.headers.authorization) : null; return { prisma, userId: token?.userId }; };5.5 保护 post 变更并测试完整认证流postresolver 校验userId存在并通过postedBy: { connect: { id: userId } }把新 Link 关联到当前用户if (!userId) { throw new Error(Cannot post without logging in.); } const newLink context.prisma.link.create({ data: { description, url, postedBy: { connect: { id: userId } }, }, });测试流程先发送signup拿到 JWT → 在 Apollo Studio 的 Headers 中添加Authorization: Bearer TOKEN→ 调用带认证的post→ 用login验证user.links已包含新创建的 Link。移除或禁用该 Header 后再次post会得到Cannot post without logging in.错误。此外运行npx prisma studio可在http://localhost:5555通过 GUI 浏览Link、User及二者关系的数据。六、投票功能与自定义标量多对多关系与 DateTime6.1 隐式多对多关系在 Prisma 模型中新增voters/votes关系字段由于User与Link之间已有PostedBy关系需用name属性区分两条关系model Link { ... postedBy User? relation(name: PostedBy, fields: [postedById], references: [id]) postedById Int? voters User[] relation(name: Votes) } model User { ... links Link[] relation(name: PostedBy) votes Link[] relation(name: Votes) }这种由 Prisma 在底层自动维护关系表join table的多对多关系称为隐式多对多关系适用于关系本身无需附加额外信息的场景若需要在关系上挂载额外数据则应使用显式多对多关系。执行npx prisma migrate dev --name add-vote-relation完成迁移。6.2 vote 变更export const VoteMutation extendType({ type: Mutation, definition(t) { t.field(vote, { type: Vote, args: { linkId: nonNull(intArg()) }, async resolve(parent, args, context) { const { userId } context; const { linkId } args; if (!userId) { throw new Error(Cannot vote without logging in.); } const link await context.prisma.link.update({ where: { id: linkId }, data: { voters: { connect: { id: userId } } }, }); const user await context.prisma.user.findUnique({ where: { id: userId } }); return { link, user: user as User }; }, }); }, });userId无需作为参数传入因为它可以从 Authorization 头解码得到connect用于把用户挂到链接的voters多对多关系上。随后在Link类型增加voters: [User!]!字段、在User类型增加votes: [Link!]!字段resolver 同样基于 Fluent API 实现。6.3 自定义标量与 DateTime 标量GraphQL 内置Int、Float、String、Boolean、ID五种标量自定义标量可同时定义数据的表示形式与校验逻辑。Nexus 底层使用graphql库的GraphQLScalarType因此 Node 生态各库定义的标量通常相互兼容。安装并接入graphql-scalars提供的DateTimenpm install graphql-scalars^1.14.1// src/graphql/scalars/Date.ts import { asNexusMethod } from nexus; import { GraphQLDateTime } from graphql-scalars; export const GQLDate asNexusMethod(GraphQLDateTime, dateTime);将scalars/Date加入src/graphql/index.ts的导出后schema.graphql会出现scalar DateTime声明遵循 RFC 3339/ISO 8601 规范与 Prisma 的DateTime类型一致。由于Link的 Prisma 模型本就带有createdAt字段只需在 Nexus 类型定义中加入t.nonNull.dateTime(createdAt)查询时即可自动解析。最终feed查询可返回createdAt字段例如2021-12-14T23:21:52.620Z。七、过滤、分页与排序把 Prisma 能力映射到 GraphQL API7.1 过滤给feed增加可选字符串参数filter在 resolver 中构造where条件让description或url包含该字符串resolve(parent, args, context) { const where args.filter ? { OR: [ { description: { contains: args.filter } }, { url: { contains: args.filter } }, ], } : {}; return context.prisma.link.findMany({ where }); }7.2 分页limit-offsetAPI 设计中常见的两种分页是 limit-offset 与 cursor-basedPrisma 两者都支持。本教程实现 limit-offsetPrisma 中 limit 对应take取多少个元素offset 对应skip跳过多少元素默认 0。args: { filter: stringArg(), skip: intArg(), take: intArg(), }, resolve(parent, args, context) { ... return context.prisma.link.findMany({ where, skip: args?.skip as number | undefined, take: args?.take as number | undefined, }); }注意 Prisma 对null与undefined语义区分严格null是具体值undefined表示不做处理。Nexus 生成类型包含null因此需要类型断言为number | undefined。7.3 排序通过inputObjectType与enumType定义排序输入export const LinkOrderByInput inputObjectType({ name: LinkOrderByInput, definition(t) { t.field(description, { type: Sort }); t.field(url, { type: Sort }); t.field(createdAt, { type: Sort }); }, }); export const Sort enumType({ name: Sort, members: [asc, desc], });生成的 SDL 为input LinkOrderByInput { createdAt: Sort description: Sort url: Sort }与enum Sort { asc desc }。feed新增orderBy: arg({ type: list(nonNull(LinkOrderByInput)) })支持按多个字段排序如orderBy: [{ url: asc }, { createdAt: desc }]resolver 中将其断言后传给 Prisma 的orderBy选项。7.4 返回总数重构为 Feed 类型为了让客户端获知符合条件的总数而非仅当前页数量feed的返回类型重构为Feedexport const Feed objectType({ name: Feed, definition(t) { t.nonNull.list.nonNull.field(links, { type: Link }); t.nonNull.int(count); t.id(id); }, });resolver 中使用 Prisma 的countAPI 统计满足过滤条件的记录数不受skip/take影响并用序列化后的查询参数生成唯一idconst links await context.prisma.link.findMany({ where, skip, take, orderBy }); const count await context.prisma.link.count({ where }); const id main-feed:${JSON.stringify(args)}; return { links, count, id };查询示例中feed(take: 1)返回count: 2但links只有 1 条直观体现总数与返回条数的区别。八、部署到生产GitHub Actions 驱动的持续部署教程最后讲解了把项目部署到 Heroku 并自动化部署流程先git init并配置.gitignore排除node_modules、.env、.vscode、dist用gh repo create hackernews-typescript --source . --private在 GitHub 创建私有远程仓库再将数据库从 SQLite 切换为 PostgreSQL教程用 Docker/Docker Compose 运行 PostgreSQL 实例也可原生安装最后编写 GitHub Actions 工作流在每次推送等可配置事件触发时自动构建并部署到 Heroku实现持续部署CD。前置条件包括 GitHub/Heroku 账号、GitHub CLI/Heroku CLI 以及 Docker 与 Docker Compose。九、最终架构与核心收获9.1 分层职责从本系列教程可以归纳出清晰的职责分层GraphQL schema 层由 Nexuscode-first定义并生成 SDL 与类型定义HTTP 服务层Apollo Server 接收并执行 GraphQL 操作负责解析请求头如Authorization并构建contextResolver 层每个字段的解析实现通过parent、args、context三个参数驱动数据访问层Prisma Client 作为类型安全的 CRUD API承载全部数据库读写其方法基于schema.prisma中的模型自动生成。正如 总结章节 所述apollo-server是快速、简单的 GraphQL 服务器库自带基于 Web 的 GraphQL 客户端等多项特性GraphQL 服务端的 resolver 使用 Prisma Client 实现由它负责数据库访问。Prisma Client 暴露了针对 schema 中所有模型的 CRUD 操作resolver 借助它执行查询与变更并将执行结果返回给客户端——这就是整个 Prisma/GraphQL 项目运行时的完整数据流。9.2 本系列沉淀的最佳实践清单类型安全贯穿全链路Nexus 保证 GraphQL SDL 与 TypeScript 类型单一来源、永不失同步Prisma Client 保证数据库模型与查询代码类型安全context 是 resolver 之间的通信桥梁可以在服务器初始化时把 Prisma Client、认证信息等写入 context供所有 resolver 读取认证先于授权JWT 放在Authorization: Bearer头中服务器解码后把userId注入 contextresolver 再做权限校验关系建模分层决策一对多关系用外键 relation无附加信息的多对多关系用隐式关系表有附加信息时改用显式关系把数据库能力按需暴露给客户端过滤containsOR、分页skip/take、排序orderBy input/enum、聚合count都可以通过少量参数映射到 GraphQL API持续部署是工程化的最后一公里版本控制 CI 工作流 云平台Heroku让 API 的每次变更自动进入生产环境。9.3 进一步深入的方向若希望继续深耕可以在本仓库中按章节顺序完整复刻整个实现从 开始章节 起步依次经过 查询、变更、接入数据库、连接服务与数据库、认证、投票与自定义标量、过滤分页排序 与 部署。也可以横向对比仓库中其他语言/技术栈的实现如 GraphQL Go、GraphQL Java、TypeScript Helix 等从而理解不同生态下 GraphQL 服务端工程的共性与差异。至此你已经完整经历了用 TypeScript Apollo Server Nexus Prisma 从零构建并部署一个生产级 GraphQL API的全过程——既掌握了 GraphQL schema、resolver、认证、关系、自定义标量与查询增强等核心机制也建立了从数据建模到持续部署的工程化全局视角。赞分享【免费下载链接】howtographqlThe Fullstack Tutorial for GraphQL项目地址https://gitcode.com/gh_mirrors/ho/howtographql点击查看免费下载相关推荐CANN/asc-devkit浮点转整型APIasc_float2int16 产品支持情况 | 产品 | 是否支持 | | : | : :| | term Atlas A3 训练系列产品/Atlas A3Windows 11终极优化指南Win11Debloat深度解析与高效配置Windows 11终极优化指南Win11Debloat深度解析与高效配置 你是否曾为Windows 11系统日益臃肿而烦恼每次系统更新后那些不请自来的A桌面应用CLIMilo v1.5 开源桌面CNC铣床装配指南从STL零件到能铣铝的成品Milo v1.5 开源桌面CNC铣床装配指南从STL零件到能铣铝的成品 主轴切下一片铝屑切屑在灯光下卷成细丝——这台开源桌面CNC铣床叫 Milo v1.上一篇Starship Plain Text Symbols 预设完全指南在无 Unicode 环境下使用纯文本符号打造跨平台提示符下一篇UDEV Gothic字体测试与质量保证确保跨平台兼容性的完整流程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考