乐闻世界logo
搜索文章和话题

NestJS GraphQL 如何集成?

2月21日 17:11

NestJS GraphQL 集成详解

GraphQL 概述

GraphQL 是一种用于 API 的查询语言和运行时环境。NestJS 通过 @nestjs/graphql 包提供了完整的 GraphQL 支持,使开发者能够构建灵活、高效的 GraphQL API。

安装依赖

bash
npm install @nestjs/graphql graphql apollo-server-express npm install -D @types/graphql

基本配置

配置 GraphQL 模块

typescript
import { 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

typescript
import { 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; }

定义输入类型

typescript
import { 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

typescript
import { 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); } }

使用自定义装饰器

typescript
import { 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[]; }

分页

基于游标的分页

typescript
import { 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)

配置订阅

typescript
import { 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

typescript
import { 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

typescript
import { 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

typescript
import { 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

typescript
import { IsEmail, IsString, MinLength } from 'class-validator'; @InputType() export class CreateUserInput { @Field() @IsEmail() email: string; @Field() @IsString() @MinLength(6) password: string; @Field() @IsString() name: string; }

自定义验证器

typescript
import { 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; }

错误处理

自定义错误类

typescript
import { 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); } } }

权限控制

创建权限守卫

typescript
import { 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; } }

使用权限守卫

typescript
import { 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); } }

性能优化

查询复杂度分析

typescript
import { 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}`); }, }), ], })

查询深度限制

typescript
import { depthLimit } from 'graphql-depth-limit'; GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, validationRules: [ depthLimit(5), ], })

最佳实践

  1. Schema 优先:优先使用 Schema 优先的方法
  2. 类型安全:充分利用 TypeScript 的类型系统
  3. 分页:始终实现分页以避免大量数据传输
  4. 数据加载器:使用 DataLoader 解决 N+1 查询问题
  5. 错误处理:提供清晰的错误信息
  6. 权限控制:实现适当的权限控制机制
  7. 性能监控:监控查询性能和复杂度
  8. 文档化:使用 GraphQL Playground 和文档工具

总结

NestJS GraphQL 集成提供了:

  • 完整的 GraphQL 支持
  • 灵活的 Schema 定义
  • 强大的类型安全
  • 丰富的查询功能
  • 易于集成的生态系统

掌握 NestJS GraphQL 集成是构建现代、灵活 API 的关键。通过合理使用 GraphQL 的特性,可以构建出高效、类型安全、易于维护的 API,满足前端对数据的精确需求。GraphQL 的查询语言特性使客户端能够精确获取所需数据,减少网络传输和提升性能。

标签:NestJS