NestJS 集成 GraphQL:代码优先模式实战指南
NestJS 对 GraphQL 的支持是所有 Node.js 框架里做得最顺的——代码优先模式下,你只写 TypeScript 类,Schema 自动生成,不用手写 .graphql 文件也不用维护两套类型定义。这篇讲清楚两种集成方式的选择、核心配置、以及 Resolver/ObjectType/InputType 的写法。
代码优先 vs Schema 优先
NestJS 支持两种 GraphQL 集成方式,90% 的项目应该选代码优先:
| 代码优先(推荐) | Schema 优先 | |
|---|---|---|
| 工作流 | 写 TypeScript → 自动生成 Schema | 写 Schema → 自动生成 TypeScript 类型 |
| 类型安全 | 天然安全,TypeScript 是单一事实来源 | 需要生成类型定义,可能和手写代码不同步 |
| 维护成本 | 低——只维护一份代码 | 高——Schema 和代码要同步 |
| 适合场景 | 纯 TypeScript 项目 | 多语言共享 Schema、已有 Schema 的项目 |
安装和基本配置
bashnpm install @nestjs/graphql @nestjs/apollo graphql @apollo/server
代码优先配置
typescript// app.module.ts import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { join } from 'path'; @Module({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: join(process.cwd(), 'src/schema.gql'), sortSchema: true, // 生成的 Schema 按字母排序,减少 git diff 噪音 playground: true, // 开发环境启用 GraphQL Playground }), ], }) export class AppModule {}
autoSchemaFile 是代码优先模式的关键——NestJS 会根据你的 TypeScript 装饰器自动生成 GraphQL Schema 文件。sortSchema: true 让生成的 Schema 内容顺序稳定,避免每次重新生成都产生无意义的 diff。
Schema 优先配置
typescriptGraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, typePaths: ['./**/*.graphql'], // 指向你的 .graphql 文件 definitions: { path: join(process.cwd(), 'src/graphql.ts'), // 生成的 TypeScript 类型 outputAs: 'class', }, })
定义 ObjectType(对应数据库模型)
ObjectType 是 GraphQL 的返回类型,相当于 REST 里的响应 DTO:
typescript// models/user.model.ts import { ObjectType, Field, Int } from '@nestjs/graphql'; @ObjectType() export class User { @Field(() => Int) id: number; @Field() email: string; @Field({ nullable: true }) nickname?: string; @Field(() => Int) age: number; @Field() createdAt: Date; }
注意:TypeScript 的 number 对应 GraphQL 的 Int 和 Float 两种类型,必须显式指定 @Field(() => Int) 或 @Field(() => Float),否则 NestJS 不知道该映射到哪个。
nullable: true 标记可空字段。默认所有字段都是非空的——这比 REST 的 JSON 响应更严格,客户端能明确知道哪些字段一定有值。
定义 InputType(对应请求参数)
InputType 是 GraphQL 的输入类型,相当于 REST 的请求 DTO:
typescript// dto/create-user.input.ts import { InputType, Field } from '@nestjs/graphql'; @InputType() export class CreateUserInput { @Field() email: string; @Field({ nullable: true }) nickname?: string; @Field(() => Int) age: number; }
ObjectType 和 InputType 的区别:ObjectType 用于返回数据,InputType 用于接收参数。GraphQL 规范要求输入和输出类型必须分开定义——虽然字段可能一样,但不能复用同一个类。
部分更新用 Patch
typescript@InputType() export class UpdateUserInput { @Field(() => Int) id: number; @Field({ nullable: true }) email?: string; @Field({ nullable: true }) nickname?: string; @Field(() => Int, { nullable: true }) age?: number; }
所有字段都设为 nullable: true,客户端只需传要更新的字段。在 Service 层判断哪些字段有值就更新哪些。
写 Resolver(对应 Controller)
Resolver 是 GraphQL 的"控制器",处理查询和变更请求:
typescript// resolvers/user.resolver.ts import { Resolver, Query, Mutation, Args } from '@nestjs/graphql'; @Resolver(() => User) export class UserResolver { constructor(private readonly userService: UserService) {} @Query(() => [User]) async users(): Promise<User[]> { return this.userService.findAll(); } @Query(() => User, { nullable: true }) async user(@Args('id', { type: () => Int }) id: number): Promise<User | null> { return this.userService.findOne(id); } @Mutation(() => User) async createUser( @Args('input') input: CreateUserInput, ): Promise<User> { return this.userService.create(input); } @Mutation(() => User) async updateUser( @Args('input') input: UpdateUserInput, ): Promise<User> { return this.userService.update(input); } @Mutation(() => Boolean) async deleteUser(@Args('id', { type: () => Int }) id: number): Promise<boolean> { return this.userService.remove(id); } }
@Query用于读取数据(对应 REST GET)@Mutation用于修改数据(对应 REST POST/PUT/DELETE)@Args提取参数,和 Controller 里的@Param/@Body类似{ nullable: true }表示查询可能返回 null(比如按 ID 查不到)
关联查询(关系型数据)
用户和文章是一对多关系,GraphQL 的优势是客户端可以按需获取关联数据:
typescript// models/post.model.ts @ObjectType() export class Post { @Field(() => Int) id: number; @Field() title: string; @Field(() => Int) authorId: number; @Field(() => User) author: User; // 关联字段 } // resolvers/post.resolver.ts @Resolver(() => Post) export class PostResolver { @ResolveField('author', () => User) async getAuthor(@Parent() post: Post): Promise<User> { return this.userService.findOne(post.authorId); } }
@ResolveField 是懒加载——只有客户端请求了 author 字段时才会执行。如果客户端只查 title,就不会触发额外的数据库查询。
N+1 问题
关联查询的陷阱是 N+1:查 10 篇文章的作者,会执行 1 次查文章 + 10 次查作者 = 11 次查询。解决方案是用 DataLoader 批量加载:
typescriptimport * as DataLoader from 'dataloader'; // 在 Module 中注册 DataLoader providers: [ { provide: 'USER_LOADER', useFactory: (userService: UserService) => { return new DataLoader(async (ids: number[]) => { const users = await userService.findByIds(ids); return ids.map(id => users.find(u => u.id === id)); }); }, inject: [UserService], }, ] // 在 Resolver 中使用 @ResolveField('author', () => User) async getAuthor( @Parent() post: Post, @Inject('USER_LOADER') userLoader: DataLoader<number, User>, ) { return userLoader.load(post.authorId); }
DataLoader 会把同一批次的所有 load() 调用合并成一次 findByIds() 查询,11 次变 2 次。
认证和授权
在 Resolver 层面守卫
typescript@UseGuards(GqlAuthGuard) @Mutation(() => Post) async createPost( @Args('input') input: CreatePostInput, @CurrentUser() user: User, ): Promise<Post> { return this.postService.create(user.id, input); }
GqlAuthGuard 需要从 GraphQL 上下文中提取用户信息,和 REST 的 AuthGuard 略有不同:
typescript@Injectable() export class GqlAuthGuard extends AuthGuard('jwt') { getRequest(context: ExecutionContext) { const ctx = GqlExecutionContext.create(context); return ctx.getContext().req; // GraphQL 请求对象 } }
分页查询
GraphQL 常用的游标分页(Relay 风格):
typescript@ObjectType() class PaginatedUsers { @Field(() => [User]) items: User[]; @Field(() => Int) totalCount: number; @Field() hasNextPage: boolean; @Field(() => String, { nullable: true }) cursor?: string; } @Query(() => PaginatedUsers) async users( @Args('limit', { type: () => Int, defaultValue: 10 }) limit: number, @Args('cursor', { type: () => String, { nullable: true } }) cursor?: string, ): Promise<PaginatedUsers> { return this.userService.findPaginated(limit, cursor); }
客户端可以灵活选择每页数量和起始位置,不像 REST 分页那么死板。
调试技巧
在 Playground 里测试
启动应用后访问 http://localhost:3000/graphql,可以直接写查询测试。比用 Postman 方便得多——左侧写查询,右侧看结果,还有自动补全。
查看生成的 Schema
代码优先模式下,autoSchemaFile 指向的文件就是生成的 Schema。如果查询报类型错误,先看看生成的 Schema 是否符合预期——可能是装饰器写错了。
常见报错
- "Cannot determine a GraphQL type for User":忘了加
@ObjectType()装饰器 - "Query root type must be provided":没有任何
@Query装饰器,Schema 是空的 - Circular dependency:两个 ObjectType 互相引用,用
() => import('./other.model').then(m => m.OtherType)延迟引用解决
快速配置清单
| 检查项 | 配置 |
|---|---|
| 选择模式 | 代码优先(autoSchemaFile) |
| Driver | ApolloDriver |
| 数字类型 | 显式指定 () => Int 或 () => Float |
| 可空字段 | { nullable: true } |
| 关联查询 | @ResolveField + DataLoader 防 N+1 |
| 认证 | GqlAuthGuard 从 GqlExecutionContext 取 req |
| Schema 排序 | sortSchema: true 减少 diff 噪音 |