NestJS GraphQL 集成详解
GraphQL 概述
GraphQL 是一种用于 API 的查询语言和运行时环境。NestJS 通过 @nestjs/graphql 包提供了完整的 GraphQL 支持,使开发者能够构建灵活、高效的 GraphQL API。
安装依赖
bashnpm install @nestjs/graphql graphql apollo-server-express npm install -D @types/graphql
基本配置
配置 GraphQL 模块
typescriptimport { Module } from '@nestjs/common'; 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, playground: true, introspection: true, context: ({ req }) => ({ req }), }), ], }) export class AppModule {}
定义 Schema
使用装饰器定义 Schema
typescriptimport { Field, Int, ObjectType, Resolver, Query, Mutation, Args, ID } from '@nestjs/graphql'; import { User } from './entities/user.entity'; @ObjectType() export class User { @Field(() => ID) id: number; @Field() name: string; @Field() email: string; @Field(() => Int) age: number; @Field() isActive: boolean; @Field() createdAt: Date; @Field() updatedAt: Date; }
定义输入类型
typescriptimport { InputType, Field } from '@nestjs/graphql'; @InputType() export class CreateUserInput { @Field() name: string; @Field() email: string; @Field() password: string; @Field(() => Int, { nullable: true }) age?: number; } @InputType() export class UpdateUserInput { @Field(() => ID) id: number; @Field({ nullable: true }) name?: string; @Field({ nullable: true }) email?: string; @Field(() => Int, { nullable: true }) age?: number; }
创建 Resolver
基本 Resolver
typescriptimport { Resolver, Query, Mutation, Args, ID } from '@nestjs/graphql'; import { UsersService } from './users.service'; import { User } from './entities/user.entity'; import { CreateUserInput, UpdateUserInput } from './dto/user.input'; @Resolver(() => User) export class UsersResolver { constructor(private usersService: UsersService) {} @Query(() => [User]) async users(): Promise<User[]> { return this.usersService.findAll(); } @Query(() => User, { nullable: true }) async user(@Args('id', { type: () => ID }) id: number): Promise<User> { return this.usersService.findOne(id); } @Mutation(() => User) async createUser(@Args('input') input: CreateUserInput): Promise<User> { return this.usersService.create(input); } @Mutation(() => User, { nullable: true }) async updateUser(@Args('input') input: UpdateUserInput): Promise<User> { return this.usersService.update(input.id, input); } @Mutation(() => Boolean) async deleteUser(@Args('id', { type: () => ID }) id: number): Promise<boolean> { return this.usersService.remove(id); } }
使用自定义装饰器
typescriptimport { createParamDecorator, ExecutionContext } from '@nestjs/common'; export const CurrentUser = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return request.user; }, ); // 在 Resolver 中使用 @Mutation(() => User) async createUser( @Args('input') input: CreateUserInput, @CurrentUser() user: any, ): Promise<User> { return this.usersService.create(input, user.id); }
关系和加载
一对一关系
typescript@ObjectType() export class Profile { @Field(() => ID) id: number; @Field() bio: string; @Field(() => User) user: User; } @ObjectType() export class User { @Field(() => ID) id: number; @Field() name: string; @Field(() => Profile, { nullable: true }) profile?: Profile; }
一对多关系
typescript@ObjectType() export class Post { @Field(() => ID) id: number; @Field() title: string; @Field() content: string; @Field(() => User) author: User; } @ObjectType() export class User { @Field(() => ID) id: number; @Field() name: string; @Field(() => [Post]) posts: Post[]; }
多对多关系
typescript@ObjectType() export class Tag { @Field(() => ID) id: number; @Field() name: string; @Field(() => [Post]) posts: Post[]; } @ObjectType() export class Post { @Field(() => ID) id: number; @Field() title: string; @Field(() => [Tag]) tags: Tag[]; }
分页
基于游标的分页
typescriptimport { ArgsType, Field, Int } from '@nestjs/graphql'; @ArgsType() export class PaginationArgs { @Field(() => Int, { nullable: true }) first?: number; @Field(() => String, { nullable: true }) after?: string; } @ObjectType() export class PaginatedUsers { @Field(() => [User]) data: User[]; @Field(() => Boolean) hasNextPage: boolean; @Field(() => String, { nullable: true }) endCursor?: string; } @Resolver(() => User) export class UsersResolver { @Query(() => PaginatedUsers) async users(@Args() pagination: PaginationArgs): Promise<PaginatedUsers> { const { data, hasNextPage, endCursor } = await this.usersService.paginate(pagination); return { data, hasNextPage, endCursor }; } }
基于偏移量的分页
typescript@ArgsType() export class OffsetPaginationArgs { @Field(() => Int, { nullable: true, defaultValue: 0 }) skip?: number; @Field(() => Int, { nullable: true, defaultValue: 10 }) take?: number; } @ObjectType() export class OffsetPaginatedUsers { @Field(() => [User]) data: User[]; @Field(() => Int) total: number; @Field(() => Int) page: number; @Field(() => Int) totalPages: number; } @Resolver(() => User) export class UsersResolver { @Query(() => OffsetPaginatedUsers) async users(@Args() pagination: OffsetPaginationArgs): Promise<OffsetPaginatedUsers> { const { data, total, page, totalPages } = await this.usersService.paginate(pagination); return { data, total, page, totalPages }; } }
订阅(Subscriptions)
配置订阅
typescriptimport { PubSub } from 'graphql-subscriptions'; @Module({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, installSubscriptionHandlers: true, context: ({ req, connection }) => { if (connection) { return { req: connection.context }; } return { req }; }, }), ], }) export class AppModule {}
创建订阅 Resolver
typescriptimport { Resolver, Subscription, Root } from '@nestjs/graphql'; import { PubSub } from 'graphql-subscriptions'; @ObjectType() export class Message { @Field(() => ID) id: number; @Field() content: string; @Field() createdAt: Date; } @Resolver(() => Message) export class MessagesResolver { private pubSub: PubSub; constructor() { this.pubSub = new PubSub(); } @Subscription(() => Message, { filter: (payload, variables) => { return payload.chatId === variables.chatId; }, }) messageAdded(@Root() message: Message, @Args('chatId') chatId: number): Message { return message; } async publishMessage(message: Message) { await this.pubSub.publish('messageAdded', { messageAdded: message }); } }
数据加载器(DataLoader)
创建 DataLoader
typescriptimport { DataLoader } from 'dataloader'; import { UsersService } from './users.service'; export class UserDataLoader { private loader: DataLoader<number, User>; constructor(private usersService: UsersService) { this.loader = new DataLoader(async (ids) => { const users = await this.usersService.findByIds(ids); return ids.map(id => users.find(user => user.id === id)); }); } async load(id: number): Promise<User> { return this.loader.load(id); } async loadMany(ids: number[]): Promise<User[]> { return this.loader.loadMany(ids); } }
在 Resolver 中使用 DataLoader
typescriptimport { Resolver, Query, Args, ID, Parent } from '@nestjs/graphql'; import { UserDataLoader } from './user-dataloader'; @Resolver(() => Post) export class PostsResolver { constructor(private userDataLoader: UserDataLoader) {} @Query(() => [Post]) async posts(): Promise<Post[]> { return this.postsService.findAll(); } @FieldResolver(() => User) async author(@Parent() post: Post): Promise<User> { return this.userDataLoader.load(post.authorId); } }
验证和转换
使用 class-validator
typescriptimport { IsEmail, IsString, MinLength } from 'class-validator'; @InputType() export class CreateUserInput { @Field() @IsEmail() email: string; @Field() @IsString() @MinLength(6) password: string; @Field() @IsString() name: string; }
自定义验证器
typescriptimport { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'; export function IsStrongPassword(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ name: 'isStrongPassword', target: object.constructor, propertyName: propertyName, options: validationOptions, validator: { validate(value: any) { const hasUpperCase = /[A-Z]/.test(value); const hasLowerCase = /[a-z]/.test(value); const hasNumber = /[0-9]/.test(value); return hasUpperCase && hasLowerCase && hasNumber; }, defaultMessage(args: ValidationArguments) { return 'Password must contain uppercase, lowercase, and numbers'; }, }, }); }; } @InputType() export class CreateUserInput { @Field() @IsStrongPassword() password: string; }
错误处理
自定义错误类
typescriptimport { ApolloError } from 'apollo-server-express'; export class UserNotFoundError extends ApolloError { constructor(id: number) { super(`User with ID ${id} not found`, 'USER_NOT_FOUND', { id, }); } } export class ValidationError extends ApolloError { constructor(message: string, fields: string[]) { super(message, 'VALIDATION_ERROR', { fields }); } }
在 Resolver 中使用错误
typescript@Resolver(() => User) export class UsersResolver { constructor(private usersService: UsersService) {} @Query(() => User, { nullable: true }) async user(@Args('id', { type: () => ID }) id: number): Promise<User> { const user = await this.usersService.findOne(id); if (!user) { throw new UserNotFoundError(id); } return user; } @Mutation(() => User) async createUser(@Args('input') input: CreateUserInput): Promise<User> { try { return await this.usersService.create(input); } catch (error) { throw new ValidationError('Invalid input', error.fields); } } }
权限控制
创建权限守卫
typescriptimport { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; @Injectable() export class GqlAuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const ctx = GqlExecutionContext.create(context); const { req } = ctx.getContext(); if (!req.user) { throw new UnauthorizedException(); } return true; } }
使用权限守卫
typescriptimport { Resolver, Mutation, UseGuards } from '@nestjs/graphql'; import { GqlAuthGuard } from './guards/gql-auth.guard'; @Resolver(() => User) export class UsersResolver { @Mutation(() => User) @UseGuards(GqlAuthGuard) async createUser(@Args('input') input: CreateUserInput): Promise<User> { return this.usersService.create(input); } }
性能优化
查询复杂度分析
typescriptimport { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, validationRules: [ queryComplexity({ maximumComplexity: 1000, variables: {}, onComplete: (complexity) => { console.log(`Query complexity: ${complexity}`); }, }), ], })
查询深度限制
typescriptimport { depthLimit } from 'graphql-depth-limit'; GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, validationRules: [ depthLimit(5), ], })
最佳实践
- Schema 优先:优先使用 Schema 优先的方法
- 类型安全:充分利用 TypeScript 的类型系统
- 分页:始终实现分页以避免大量数据传输
- 数据加载器:使用 DataLoader 解决 N+1 查询问题
- 错误处理:提供清晰的错误信息
- 权限控制:实现适当的权限控制机制
- 性能监控:监控查询性能和复杂度
- 文档化:使用 GraphQL Playground 和文档工具
总结
NestJS GraphQL 集成提供了:
- 完整的 GraphQL 支持
- 灵活的 Schema 定义
- 强大的类型安全
- 丰富的查询功能
- 易于集成的生态系统
掌握 NestJS GraphQL 集成是构建现代、灵活 API 的关键。通过合理使用 GraphQL 的特性,可以构建出高效、类型安全、易于维护的 API,满足前端对数据的精确需求。GraphQL 的查询语言特性使客户端能够精确获取所需数据,减少网络传输和提升性能。