NestJS GraphQL实战教程
学习目标
- 掌握GraphQL的基本概念和工作原理
- 学会在NestJS中集成GraphQL
- 理解GraphQL Schema和Resolver的定义方法
- 掌握实现CRUD操作的GraphQL查询和变更
- 学会处理GraphQL中的关系和嵌套查询
- 实现GraphQL的认证和授权
- 了解GraphQL的性能优化策略
- 掌握GraphQL的部署和最佳实践
核心概念
GraphQL简介
GraphQL是一种用于API的查询语言,也是一个满足你数据查询的运行时。它提供了一种更高效、强大和灵活的API开发方式,相比传统的REST API,GraphQL具有以下优势:
- 按需获取数据:客户端可以精确指定需要的字段,避免过度获取或获取不足
- 单一端点:所有操作都通过单一端点执行,简化API设计
- 类型系统:强类型的Schema确保数据的一致性和可靠性
- 实时更新:支持订阅(Subscription)实现实时数据更新
- 自文档化: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部署到服务器
- 使用PM2管理进程:
# 安装PM2
npm i -g pm2
# 启动应用
npm run start:prod
# 或者使用PM2
pm run build
pm run start:prod- 使用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设计
- 使用有意义的类型和字段名称:保持命名一致和清晰
- 合理使用接口和联合类型:提高Schema的灵活性
- 添加描述字段:使用GraphQL的描述功能提高文档质量
- 设计合理的分页策略:使用游标或偏移分页
- 避免深度嵌套查询:设置查询深度限制
Resolver实现
- 使用DataLoader:解决N+1查询问题
- 实现缓存:对频繁查询的数据进行缓存
- 合理使用批量操作:减少数据库查询次数
- 错误处理:提供清晰的错误信息
- 认证和授权:保护敏感操作
性能优化
- 查询分析:使用Apollo Server的查询分析工具
- 缓存策略:实现Redis缓存
- 查询限制:设置最大查询深度和复杂度
- 数据预取:在Resolver中预取相关数据
- 批量操作:使用数据库的批量操作
安全性
- 认证和授权:保护API端点
- 输入验证:验证所有用户输入
- 查询限制:防止恶意查询
- 数据过滤:确保用户只能访问有权限的数据
- HTTPS:使用HTTPS保护传输层
总结
本教程详细讲解了如何在NestJS中集成和使用GraphQL,包括:
- GraphQL的基本概念和工作原理
- 在NestJS中集成GraphQL
- 定义GraphQL Schema和Resolver
- 实现CRUD操作的GraphQL查询和变更
- 处理GraphQL中的关系和嵌套查询
- 实现GraphQL的认证和授权
- GraphQL的性能优化策略
- 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查询性能。
实践作业
实现更复杂的关系查询:
- 添加帖子分类功能
- 实现多对多关系的GraphQL查询
优化查询性能:
- 集成DataLoader解决N+1查询问题
- 实现Redis缓存
添加更多高级特性:
- 实现GraphQL订阅
- 添加自定义标量类型
- 实现指令(Directives)
完善错误处理:
- 创建自定义GraphQL异常
- 实现全局异常过滤器
部署到生产环境:
- 配置生产环境变量
- 实现监控和日志
- 配置CI/CD管道
通过完成这些作业,你将能够进一步巩固所学知识,开发出功能更加完善和高性能的GraphQL API。