6月5日 20:02

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 的项目

安装和基本配置

bash
npm 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 优先配置

typescript
GraphQLModule.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 的 IntFloat 两种类型,必须显式指定 @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 批量加载:

typescript
import * 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
DriverApolloDriver
数字类型显式指定 () => Int() => Float
可空字段{ nullable: true }
关联查询@ResolveField + DataLoader 防 N+1
认证GqlAuthGuardGqlExecutionContext 取 req
Schema 排序sortSchema: true 减少 diff 噪音
标签:NestJS