NestJS GraphQL实战教程

学习目标

  • 掌握GraphQL的基本概念和工作原理
  • 学会在NestJS中集成GraphQL
  • 理解GraphQL Schema和Resolver的定义方法
  • 掌握实现CRUD操作的GraphQL查询和变更
  • 学会处理GraphQL中的关系和嵌套查询
  • 实现GraphQL的认证和授权
  • 了解GraphQL的性能优化策略
  • 掌握GraphQL的部署和最佳实践

核心概念

GraphQL简介

GraphQL是一种用于API的查询语言,也是一个满足你数据查询的运行时。它提供了一种更高效、强大和灵活的API开发方式,相比传统的REST API,GraphQL具有以下优势:

  1. 按需获取数据:客户端可以精确指定需要的字段,避免过度获取或获取不足
  2. 单一端点:所有操作都通过单一端点执行,简化API设计
  3. 类型系统:强类型的Schema确保数据的一致性和可靠性
  4. 实时更新:支持订阅(Subscription)实现实时数据更新
  5. 自文档化:Schema本身就是API的文档

GraphQL核心概念

  • Schema:定义API的类型系统和操作
  • Query:用于获取数据的操作(类似REST的GET)
  • Mutation:用于修改数据的操作(类似REST的POST/PUT/DELETE)
  • Subscription:用于订阅实时数据更新的操作
  • Resolver:处理GraphQL操作的函数,负责从数据源获取数据
  • Type:定义数据模型的类型
  • Field:类型中的字段
  • Argument:操作的参数

项目初始化

创建项目

# 创建NestJS项目
npm i -g @nestjs/cli
nest new graphql-practical

# 进入项目目录
cd graphql-practical

# 安装GraphQL相关依赖
npm install @nestjs/graphql graphql-tools graphql apollo-server-express type-graphql

# 安装其他依赖
npm install @nestjs/typeorm typeorm pg @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt @nestjs/config

配置GraphQL

修改 src/app.module.ts 文件,添加GraphQL配置:

import {
  Module,
} from '@nestjs/common';
import {
  GraphQLModule,
} from '@nestjs/graphql';
import {
  ApolloDriver,
  ApolloDriverConfig,
} from '@nestjs/apollo';
import {
  TypeOrmModule,
} from '@nestjs/typeorm';
import {
  ConfigModule,
  ConfigService,
} from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get('DATABASE_HOST'),
        port: parseInt(configService.get('DATABASE_PORT')),
        username: configService.get('DATABASE_USERNAME'),
        password: configService.get('DATABASE_PASSWORD'),
        database: configService.get('DATABASE_NAME'),
        entities: [],
        synchronize: true,
      }),
      inject: [ConfigService],
    }),
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: 'schema.gql',
      playground: true,
      introspection: true,
    }),
  ],
})
export class AppModule {}

核心模块实现

用户模块

用户实体

创建 src/users/entities/user.entity.ts

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  OneToMany,
} from 'typeorm';
import { Post } from '../../posts/entities/post.entity';
import { Comment } from '../../comments/entities/comment.entity';
import { ObjectType, Field, ID, Int } from '@nestjs/graphql';

export enum UserRole {
  ADMIN = 'admin',
  USER = 'user',
}

@ObjectType()
@Entity('users')
export class User {
  @Field(() => ID)
  @PrimaryGeneratedColumn()
  id: number;

  @Field()
  @Column({ unique: true })
  username: string;

  @Field()
  @Column({ unique: true })
  email: string;

  @Column()
  password: string;

  @Field(() => String)
  @Column({ default: UserRole.USER })
  role: UserRole;

  @Field({ nullable: true })
  @Column({ nullable: true })
  avatar: string;

  @Field(() => Date)
  @CreateDateColumn()
  createdAt: Date;

  @Field(() => Date)
  @UpdateDateColumn()
  updatedAt: Date;

  @Field(() => [Post], { nullable: true })
  @OneToMany(() => Post, (post) => post.author)
  posts: Post[];

  @Field(() => [Comment], { nullable: true })
  @OneToMany(() => Comment, (comment) => comment.user)
  comments: Comment[];
}

用户DTO

创建 src/users/dto/create-user.input.ts

import {
  InputType,
  Field,
} from '@nestjs/graphql';

@InputType()
export class CreateUserInput {
  @Field()
  username: string;

  @Field()
  email: string;

  @Field()
  password: string;
}

创建 src/users/dto/update-user.input.ts

import {
  InputType,
  Field,
  ID,
  PartialType,
} from '@nestjs/graphql';
import { CreateUserInput } from './create-user.input';

@InputType()
export class UpdateUserInput extends PartialType(CreateUserInput) {
  @Field(() => ID)
  id: number;
}

创建 src/users/dto/login.input.ts

import {
  InputType,
  Field,
} from '@nestjs/graphql';

@InputType()
export class LoginInput {
  @Field()
  email: string;

  @Field()
  password: string;
}

用户服务

创建 src/users/users.service.ts

import {
  Injectable,
  ConflictException,
  UnauthorizedException,
} from '@nestjs/common';
import {
  InjectRepository,
} from '@nestjs/typeorm';
import {
  Repository,
} from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';
import { LoginInput } from './dto/login.input';
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
    private jwtService: JwtService,
  ) {}

  async create(createUserInput: CreateUserInput) {
    // 检查用户是否存在
    const existingUser = await this.usersRepository.findOne({
      where: [{ username: createUserInput.username }, { email: createUserInput.email }],
    });

    if (existingUser) {
      throw new ConflictException('用户名或邮箱已存在');
    }

    // 密码加密
    const hashedPassword = await bcrypt.hash(createUserInput.password, 10);

    // 创建用户
    const user = this.usersRepository.create({
      ...createUserInput,
      password: hashedPassword,
    });

    return await this.usersRepository.save(user);
  }

  async findAll() {
    return await this.usersRepository.find({
      relations: ['posts', 'comments'],
    });
  }

  async findOne(id: number) {
    return await this.usersRepository.findOne({
      where: { id },
      relations: ['posts', 'comments'],
    });
  }

  async findByEmail(email: string) {
    return await this.usersRepository.findOne({
      where: { email },
    });
  }

  async update(id: number, updateUserInput: UpdateUserInput) {
    const user = await this.usersRepository.findOne({
      where: { id },
    });

    if (!user) {
      throw new UnauthorizedException('用户不存在');
    }

    // 如果更新密码,需要加密
    if (updateUserInput.password) {
      updateUserInput.password = await bcrypt.hash(updateUserInput.password, 10);
    }

    return await this.usersRepository.save({
      ...user,
      ...updateUserInput,
    });
  }

  async remove(id: number) {
    return await this.usersRepository.delete(id);
  }

  async login(loginInput: LoginInput) {
    // 查找用户
    const user = await this.usersRepository.findOne({
      where: { email: loginInput.email },
    });

    if (!user) {
      throw new UnauthorizedException('邮箱或密码错误');
    }

    // 验证密码
    const isPasswordValid = await bcrypt.compare(loginInput.password, user.password);
    if (!isPasswordValid) {
      throw new UnauthorizedException('邮箱或密码错误');
    }

    // 生成JWT令牌
    const payload = { userId: user.id, role: user.role };
    const token = this.jwtService.sign(payload);

    return {
      access_token: token,
      user,
    };
  }
}

用户Resolver

创建 src/users/users.resolver.ts

import {
  Resolver,
  Query,
  Mutation,
  Args,
  Int,
  ID,
  Context,
} from '@nestjs/graphql';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
import { CreateUserInput } from './dto/create-user.input';
import { UpdateUserInput } from './dto/update-user.input';
import { LoginInput } from './dto/login.input';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from '../auth/gql-auth.guard';

@Resolver(() => User)
export class UsersResolver {
  constructor(private usersService: UsersService) {}

  // 注册
  @Mutation(() => User)
  async createUser(@Args('createUserInput') createUserInput: CreateUserInput) {
    return await this.usersService.create(createUserInput);
  }

  // 登录
  @Mutation(() => Object)
  async login(@Args('loginInput') loginInput: LoginInput) {
    return await this.usersService.login(loginInput);
  }

  // 获取所有用户
  @Query(() => [User], { name: 'users' })
  @UseGuards(GqlAuthGuard)
  async findAll() {
    return await this.usersService.findAll();
  }

  // 获取单个用户
  @Query(() => User, { name: 'user' })
  @UseGuards(GqlAuthGuard)
  async findOne(@Args('id', { type: () => Int }) id: number) {
    return await this.usersService.findOne(id);
  }

  // 获取当前用户
  @Query(() => User, { name: 'me' })
  @UseGuards(GqlAuthGuard)
  async getCurrentUser(@Context() context) {
    return await this.usersService.findOne(context.req.user.userId);
  }

  // 更新用户
  @Mutation(() => User)
  @UseGuards(GqlAuthGuard)
  async updateUser(@Args('updateUserInput') updateUserInput: UpdateUserInput) {
    return await this.usersService.update(updateUserInput.id, updateUserInput);
  }

  // 删除用户
  @Mutation(() => Boolean)
  @UseGuards(GqlAuthGuard)
  async removeUser(@Args('id', { type: () => Int }) id: number) {
    const result = await this.usersService.remove(id);
    return result.affected > 0;
  }
}

帖子模块

帖子实体

创建 src/posts/entities/post.entity.ts

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToOne,
  OneToMany,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Comment } from '../../comments/entities/comment.entity';
import { ObjectType, Field, ID } from '@nestjs/graphql';

export enum PostStatus {
  DRAFT = 'draft',
  PUBLISHED = 'published',
}

@ObjectType()
@Entity('posts')
export class Post {
  @Field(() => ID)
  @PrimaryGeneratedColumn()
  id: number;

  @Field()
  @Column()
  title: string;

  @Field()
  @Column()
  content: string;

  @Field()
  @Column()
  summary: string;

  @Field(() => String)
  @Column({ default: PostStatus.DRAFT })
  status: PostStatus;

  @Field({ nullable: true })
  @Column({ nullable: true })
  coverImage: string;

  @Field(() => Date)
  @CreateDateColumn()
  createdAt: Date;

  @Field(() => Date)
  @UpdateDateColumn()
  updatedAt: Date;

  @Field(() => User)
  @ManyToOne(() => User, (user) => user.posts)
  author: User;

  @Field(() => [Comment], { nullable: true })
  @OneToMany(() => Comment, (comment) => comment.post)
  comments: Comment[];
}

帖子DTO

创建 src/posts/dto/create-post.input.ts

import {
  InputType,
  Field,
}
 from '@nestjs/graphql';

@InputType()
export class CreatePostInput {
  @Field()
  title: string;

  @Field()
  content: string;

  @Field()
  summary: string;

  @Field({ nullable: true })
  status?: string;

  @Field({ nullable: true })
  coverImage?: string;
}

创建 src/posts/dto/update-post.input.ts

import {
  InputType,
  Field,
  ID,
  PartialType,
} from '@nestjs/graphql';
import { CreatePostInput } from './create-post.input';

@InputType()
export class UpdatePostInput extends PartialType(CreatePostInput) {
  @Field(() => ID)
  id: number;
}

帖子服务

创建 src/posts/posts.service.ts

import {
  Injectable,
  NotFoundException,
  ForbiddenException,
} from '@nestjs/common';
import {
  InjectRepository,
} from '@nestjs/typeorm';
import {
  Repository,
} from 'typeorm';
import { Post } from './entities/post.entity';
import { CreatePostInput } from './dto/create-post.input';
import { UpdatePostInput } from './dto/update-post.input';

@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(Post)
    private postsRepository: Repository<Post>,
  ) {}

  async create(userId: number, createPostInput: CreatePostInput) {
    const post = this.postsRepository.create({
      ...createPostInput,
      author: { id: userId },
    });

    return await this.postsRepository.save(post);
  }

  async findAll() {
    return await this.postsRepository.find({
      relations: ['author', 'comments', 'comments.user'],
    });
  }

  async findOne(id: number) {
    return await this.postsRepository.findOne({
      where: { id },
      relations: ['author', 'comments', 'comments.user'],
    });
  }

  async update(id: number, userId: number, updatePostInput: UpdatePostInput) {
    const post = await this.postsRepository.findOne({
      where: { id },
      relations: ['author'],
    });

    if (!post) {
      throw new NotFoundException('帖子不存在');
    }

    if (post.author.id !== userId) {
      throw new ForbiddenException('无权限修改此帖子');
    }

    return await this.postsRepository.save({
      ...post,
      ...updatePostInput,
    });
  }

  async remove(id: number, userId: number) {
    const post = await this.postsRepository.findOne({
      where: { id },
      relations: ['author'],
    });

    if (!post) {
      throw new NotFoundException('帖子不存在');
    }

    if (post.author.id !== userId) {
      throw new ForbiddenException('无权限删除此帖子');
    }

    return await this.postsRepository.delete(id);
  }
}

帖子Resolver

创建 src/posts/posts.resolver.ts

import {
  Resolver,
  Query,
  Mutation,
  Args,
  Int,
  Context,
} from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post } from './entities/post.entity';
import { CreatePostInput } from './dto/create-post.input';
import { UpdatePostInput } from './dto/update-post.input';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from '../auth/gql-auth.guard';

@Resolver(() => Post)
export class PostsResolver {
  constructor(private postsService: PostsService) {}

  // 创建帖子
  @Mutation(() => Post)
  @UseGuards(GqlAuthGuard)
  async createPost(
    @Args('createPostInput') createPostInput: CreatePostInput,
    @Context() context
  ) {
    return await this.postsService.create(context.req.user.userId, createPostInput);
  }

  // 获取所有帖子
  @Query(() => [Post], { name: 'posts' })
  async findAll() {
    return await this.postsService.findAll();
  }

  // 获取单个帖子
  @Query(() => Post, { name: 'post' })
  async findOne(@Args('id', { type: () => Int }) id: number) {
    return await this.postsService.findOne(id);
  }

  // 更新帖子
  @Mutation(() => Post)
  @UseGuards(GqlAuthGuard)
  async updatePost(
    @Args('updatePostInput') updatePostInput: UpdatePostInput,
    @Context() context
  ) {
    return await this.postsService.update(updatePostInput.id, context.req.user.userId, updatePostInput);
  }

  // 删除帖子
  @Mutation(() => Boolean)
  @UseGuards(GqlAuthGuard)
  async removePost(
    @Args('id', { type: () => Int }) id: number,
    @Context() context
  ) {
    const result = await this.postsService.remove(id, context.req.user.userId);
    return result.affected > 0;
  }
}

评论模块

评论实体

创建 src/comments/entities/comment.entity.ts

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToOne,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Post } from '../../posts/entities/post.entity';
import { ObjectType, Field, ID } from '@nestjs/graphql';

@ObjectType()
@Entity('comments')
export class Comment {
  @Field(() => ID)
  @PrimaryGeneratedColumn()
  id: number;

  @Field()
  @Column()
  content: string;

  @Field(() => Date)
  @CreateDateColumn()
  createdAt: Date;

  @Field(() => Date)
  @UpdateDateColumn()
  updatedAt: Date;

  @Field(() => User)
  @ManyToOne(() => User, (user) => user.comments)
  user: User;

  @Field(() => Post)
  @ManyToOne(() => Post, (post) => post.comments)
  post: Post;
}

评论DTO

创建 src/comments/dto/create-comment.input.ts

import {
  InputType,
  Field,
  Int,
} from '@nestjs/graphql';

@InputType()
export class CreateCommentInput {
  @Field()
  content: string;

  @Field(() => Int)
  postId: number;
}

创建 src/comments/dto/update-comment.input.ts

import {
  InputType,
  Field,
  ID,
  PartialType,
} from '@nestjs/graphql';
import { CreateCommentInput } from './create-comment.input';

@InputType()
export class UpdateCommentInput extends PartialType(CreateCommentInput) {
  @Field(() => ID)
  id: number;
}

评论服务

创建 src/comments/comments.service.ts

import {
  Injectable,
  NotFoundException,
  ForbiddenException,
} from '@nestjs/common';
import {
  InjectRepository,
} from '@nestjs/typeorm';
import {
  Repository,
} from 'typeorm';
import { Comment } from './entities/comment.entity';
import { CreateCommentInput } from './dto/create-comment.input';
import { UpdateCommentInput } from './dto/update-comment.input';

@Injectable()
export class CommentsService {
  constructor(
    @InjectRepository(Comment)
    private commentsRepository: Repository<Comment>,
  ) {}

  async create(userId: number, createCommentInput: CreateCommentInput) {
    const comment = this.commentsRepository.create({
      ...createCommentInput,
      user: { id: userId },
      post: { id: createCommentInput.postId },
    });

    return await this.commentsRepository.save(comment);
  }

  async findAll() {
    return await this.commentsRepository.find({
      relations: ['user', 'post'],
    });
  }

  async findOne(id: number) {
    return await this.commentsRepository.findOne({
      where: { id },
      relations: ['user', 'post'],
    });
  }

  async update(id: number, userId: number, updateCommentInput: UpdateCommentInput) {
    const comment = await this.commentsRepository.findOne({
      where: { id },
      relations: ['user'],
    });

    if (!comment) {
      throw new NotFoundException('评论不存在');
    }

    if (comment.user.id !== userId) {
      throw new ForbiddenException('无权限修改此评论');
    }

    return await this.commentsRepository.save({
      ...comment,
      ...updateCommentInput,
    });
  }

  async remove(id: number, userId: number) {
    const comment = await this.commentsRepository.findOne({
      where: { id },
      relations: ['user'],
    });

    if (!comment) {
      throw new NotFoundException('评论不存在');
    }

    if (comment.user.id !== userId) {
      throw new ForbiddenException('无权限删除此评论');
    }

    return await this.commentsRepository.delete(id);
  }
}

评论Resolver

创建 src/comments/comments.resolver.ts

import {
  Resolver,
  Query,
  Mutation,
  Args,
  Int,
  Context,
} from '@nestjs/graphql';
import { CommentsService } from './comments.service';
import { Comment } from './entities/comment.entity';
import { CreateCommentInput } from './dto/create-comment.input';
import { UpdateCommentInput } from './dto/update-comment.input';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from '../auth/gql-auth.guard';

@Resolver(() => Comment)
export class CommentsResolver {
  constructor(private commentsService: CommentsService) {}

  // 创建评论
  @Mutation(() => Comment)
  @UseGuards(GqlAuthGuard)
  async createComment(
    @Args('createCommentInput') createCommentInput: CreateCommentInput,
    @Context() context
  ) {
    return await this.commentsService.create(context.req.user.userId, createCommentInput);
  }

  // 获取所有评论
  @Query(() => [Comment], { name: 'comments' })
  async findAll() {
    return await this.commentsService.findAll();
  }

  // 获取单个评论
  @Query(() => Comment, { name: 'comment' })
  async findOne(@Args('id', { type: () => Int }) id: number) {
    return await this.commentsService.findOne(id);
  }

  // 更新评论
  @Mutation(() => Comment)
  @UseGuards(GqlAuthGuard)
  async updateComment(
    @Args('updateCommentInput') updateCommentInput: UpdateCommentInput,
    @Context() context
  ) {
    return await this.commentsService.update(updateCommentInput.id, context.req.user.userId, updateCommentInput);
  }

  // 删除评论
  @Mutation(() => Boolean)
  @UseGuards(GqlAuthGuard)
  async removeComment(
    @Args('id', { type: () => Int }) id: number,
    @Context() context
  ) {
    const result = await this.commentsService.remove(id, context.req.user.userId);
    return result.affected > 0;
  }
}

认证模块

GraphQL认证Guard

创建 src/auth/gql-auth.guard.ts

import {
  Injectable,
  ExecutionContext,
} from '@nestjs/common';
import {
  GqlExecutionContext,
} from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req;
  }
}

JWT策略

创建 src/auth/jwt.strategy.ts

import {
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import {
  PassportStrategy,
} from '@nestjs/passport';
import {
  ExtractJwt,
  Strategy,
} from 'passport-jwt';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get('JWT_SECRET'),
    });
  }

  async validate(payload: any) {
    return {
      userId: payload.userId,
      role: payload.role,
    };
  }
}

认证模块

创建 src/auth/auth.module.ts

import {
  Module,
} from '@nestjs/common';
import {
  JwtModule,
} from '@nestjs/jwt';
import {
  PassportModule,
} from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
import { GqlAuthGuard } from './gql-auth.guard';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    PassportModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get('JWT_SECRET'),
        signOptions: { expiresIn: configService.get('JWT_EXPIRES_IN') },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [JwtStrategy, GqlAuthGuard],
  exports: [JwtStrategy, GqlAuthGuard],
})
export class AuthModule {}

主模块配置

创建 src/app.module.ts

import {
  Module,
} from '@nestjs/common';
import {
  GraphQLModule,
} from '@nestjs/graphql';
import {
  ApolloDriver,
  ApolloDriverConfig,
} from '@nestjs/apollo';
import {
  TypeOrmModule,
} from '@nestjs/typeorm';
import {
  ConfigModule,
  ConfigService,
} from '@nestjs/config';
import { UsersModule } from './users/users.module';
import { PostsModule } from './posts/posts.module';
import { CommentsModule } from './comments/comments.module';
import { AuthModule } from './auth/auth.module';
import { User } from './users/entities/user.entity';
import { Post } from './posts/entities/post.entity';
import { Comment } from './comments/entities/comment.entity';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get('DATABASE_HOST'),
        port: parseInt(configService.get('DATABASE_PORT')),
        username: configService.get('DATABASE_USERNAME'),
        password: configService.get('DATABASE_PASSWORD'),
        database: configService.get('DATABASE_NAME'),
        entities: [User, Post, Comment],
        synchronize: true,
      }),
      inject: [ConfigService],
    }),
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: 'schema.gql',
      playground: true,
      introspection: true,
      context: ({ req }) => ({ req }),
    }),
    UsersModule,
    PostsModule,
    CommentsModule,
    AuthModule,
  ],
})
export class AppModule {}

运行项目

启动开发服务器

# 启动开发服务器
npm run start:dev

访问GraphQL Playground

打开浏览器访问 http://localhost:3000/graphql,你将看到GraphQL Playground界面,可以在这里测试GraphQL查询和变更。

测试GraphQL操作

注册用户

mutation {
  createUser(createUserInput: {
    username: "testuser"
    email: "test@example.com"
    password: "password123"
  }) {
    id
    username
    email
    role
    createdAt
  }
}

登录

mutation {
  login(loginInput: {
    email: "test@example.com"
    password: "password123"
  }) {
    access_token
    user {
      id
      username
      email
      role
    }
  }
}

创建帖子

(需要在HTTP HEADERS中添加Authorization: Bearer

mutation {
  createPost(createPostInput: {
    title: "GraphQL入门教程"
    content: "GraphQL是一种强大的API查询语言..."
    summary: "GraphQL基础概念和使用方法"
    status: "published"
  }) {
    id
    title
    content
    summary
    status
    author {
      id
      username
    }
    createdAt
  }
}

获取帖子列表

query {
  posts {
    id
    title
    summary
    status
    author {
      id
      username
    }
    comments {
      id
      content
      user {
        id
        username
      }
    }
    createdAt
  }
}

创建评论

mutation {
  createComment(createCommentInput: {
    content: "这篇文章很棒!"
    postId: 1
  }) {
    id
    content
    user {
      id
      username
    }
    post {
      id
      title
    }
    createdAt
  }
}

获取当前用户

query {
  me {
    id
    username
    email
    role
    posts {
      id
      title
      status
    }
    comments {
      id
      content
    }
  }
}

高级特性

分页查询

实现分页

修改 src/posts/posts.service.ts

async findAllWithPagination(page: number = 1, limit: number = 10) {
  const skip = (page - 1) * limit;

  const [posts, total] = await this.postsRepository.findAndCount({
    relations: ['author', 'comments', 'comments.user'],
    skip,
    take: limit,
    order: { createdAt: 'DESC' },
  });

  return {
    posts,
    total,
    page,
    limit,
    pages: Math.ceil(total / limit),
  };
}

修改 src/posts/posts.resolver.ts

@Query(() => Object, { name: 'postsWithPagination' })
async findAllWithPagination(
  @Args('page', { type: () => Int, defaultValue: 1 }) page: number,
  @Args('limit', { type: () => Int, defaultValue: 10 }) limit: number
) {
  return await this.postsService.findAllWithPagination(page, limit);
}

订阅(Subscription)

实现实时评论

修改 src/comments/comments.resolver.ts

import {
  Resolver,
  Query,
  Mutation,
  Args,
  Int,
  Context,
  Subscription,
  PubSub,
  Inject,
} from '@nestjs/graphql';
import { PubSub as PubSubType } from 'graphql-subscriptions';

@Resolver(() => Comment)
export class CommentsResolver {
  constructor(
    private commentsService: CommentsService,
    @Inject('PUB_SUB') private pubSub: PubSubType
  ) {}

  // 创建评论
  @Mutation(() => Comment)
  @UseGuards(GqlAuthGuard)
  async createComment(
    @Args('createCommentInput') createCommentInput: CreateCommentInput,
    @Context() context
  ) {
    const comment = await this.commentsService.create(context.req.user.userId, createCommentInput);
    
    // 发布评论创建事件
    this.pubSub.publish('commentCreated', { commentCreated: comment });
    
    return comment;
  }

  // 订阅评论创建
  @Subscription(() => Comment, { name: 'commentCreated' })
  commentCreated() {
    return this.pubSub.asyncIterator('commentCreated');
  }
}

错误处理

自定义异常过滤器

创建 src/common/filters/graphql-exception.filter.ts

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
} from '@nestjs/common';
import {
  GqlArgumentsHost,
  GqlExceptionFilter,
} from '@nestjs/graphql';

@Catch(HttpException)
export class GraphqlExceptionFilter implements GqlExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const gqlHost = GqlArgumentsHost.create(host);
    return exception;
  }
}

性能优化

数据加载器(DataLoader)

DataLoader可以解决GraphQL中的N+1查询问题,提高查询性能。

创建 src/common/dataloaders/user.loader.ts

import {
  Injectable,
  Scope,
} from '@nestjs/common';
import {
  DataLoader,
} from 'dataloader';
import { UsersService } from '../../users/users.service';

@Injectable({ scope: Scope.REQUEST })
export class UserLoader {
  constructor(private usersService: UsersService) {}

  batchUsers = new DataLoader<number, any>(async (userIds) => {
    const users = await this.usersService.findAll();
    const userMap = new Map(users.map(user => [user.id, user]));
    return userIds.map(id => userMap.get(id));
  });
}

部署上线

构建项目

# 构建生产版本
npm run build

部署到服务器

  1. 使用PM2管理进程
# 安装PM2
npm i -g pm2

# 启动应用
npm run start:prod

# 或者使用PM2
pm run build
pm run start:prod
  1. 使用Docker部署

创建 Dockerfile

FROM node:16-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

RUN npm run build

EXPOSE 3000

CMD ["npm", "run", "start:prod"]

创建 docker-compose.yml

version: '3'
services:
  app:
    build: .
    ports:
      - '3000:3000'
    depends_on:
      - db
    environment:
      - DATABASE_HOST=db
      - DATABASE_PORT=5432
      - DATABASE_USERNAME=postgres
      - DATABASE_PASSWORD=postgres
      - DATABASE_NAME=graphql_app
      - JWT_SECRET=your-secret-key
      - JWT_EXPIRES_IN=3600

  db:
    image: postgres:13
    ports:
      - '5432:5432'
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=graphql_app
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  postgres-data:

启动Docker容器:

docker-compose up -d

最佳实践

Schema设计

  1. 使用有意义的类型和字段名称:保持命名一致和清晰
  2. 合理使用接口和联合类型:提高Schema的灵活性
  3. 添加描述字段:使用GraphQL的描述功能提高文档质量
  4. 设计合理的分页策略:使用游标或偏移分页
  5. 避免深度嵌套查询:设置查询深度限制

Resolver实现

  1. 使用DataLoader:解决N+1查询问题
  2. 实现缓存:对频繁查询的数据进行缓存
  3. 合理使用批量操作:减少数据库查询次数
  4. 错误处理:提供清晰的错误信息
  5. 认证和授权:保护敏感操作

性能优化

  1. 查询分析:使用Apollo Server的查询分析工具
  2. 缓存策略:实现Redis缓存
  3. 查询限制:设置最大查询深度和复杂度
  4. 数据预取:在Resolver中预取相关数据
  5. 批量操作:使用数据库的批量操作

安全性

  1. 认证和授权:保护API端点
  2. 输入验证:验证所有用户输入
  3. 查询限制:防止恶意查询
  4. 数据过滤:确保用户只能访问有权限的数据
  5. HTTPS:使用HTTPS保护传输层

总结

本教程详细讲解了如何在NestJS中集成和使用GraphQL,包括:

  1. GraphQL的基本概念和工作原理
  2. 在NestJS中集成GraphQL
  3. 定义GraphQL Schema和Resolver
  4. 实现CRUD操作的GraphQL查询和变更
  5. 处理GraphQL中的关系和嵌套查询
  6. 实现GraphQL的认证和授权
  7. GraphQL的性能优化策略
  8. GraphQL的部署和最佳实践

通过本教程的学习,你应该能够:

  • 理解GraphQL的核心概念和优势
  • 掌握在NestJS中开发GraphQL API的方法
  • 实现复杂的GraphQL查询和变更
  • 处理GraphQL中的关系和嵌套查询
  • 确保GraphQL API的安全性和性能

互动问答

问题1:GraphQL相比REST API有哪些优势?

答案:GraphQL相比REST API的优势包括:按需获取数据、单一端点、强类型系统、实时更新、自文档化等。

问题2:如何在NestJS中实现GraphQL认证?

答案:可以使用JWT令牌,创建GraphQL特定的认证Guard,在resolver中使用@UseGuards装饰器保护需要认证的操作。

问题3:如何解决GraphQL中的N+1查询问题?

答案:可以使用DataLoader库,它可以批量处理相同类型的查询,减少数据库查询次数。

问题4:如何实现GraphQL的实时更新?

答案:可以使用GraphQL的Subscription操作,结合PubSub实现实时数据更新。

问题5:如何优化GraphQL查询性能?

答案:可以通过使用DataLoader、实现缓存、设置查询限制、数据预取、批量操作等方式优化GraphQL查询性能。

实践作业

  1. 实现更复杂的关系查询

    • 添加帖子分类功能
    • 实现多对多关系的GraphQL查询
  2. 优化查询性能

    • 集成DataLoader解决N+1查询问题
    • 实现Redis缓存
  3. 添加更多高级特性

    • 实现GraphQL订阅
    • 添加自定义标量类型
    • 实现指令(Directives)
  4. 完善错误处理

    • 创建自定义GraphQL异常
    • 实现全局异常过滤器
  5. 部署到生产环境

    • 配置生产环境变量
    • 实现监控和日志
    • 配置CI/CD管道

通过完成这些作业,你将能够进一步巩固所学知识,开发出功能更加完善和高性能的GraphQL API。

« 上一篇 NestJS实时聊天应用开发实战教程 下一篇 » NestJS微服务实战教程