Chuyển đến nội dung chính

Lesson 15: GraphQL with NestJS

GraphQL API with NestJS. Code-first approach, Resolvers, Queries, Mutations, Subscriptions, DataLoader, Authentication.

💻 Programming — Lesson 15 Lesson 15: GraphQL with NestJS

NestJS: From Basics to Advanced

Part 4: Advanced Features

xdev.asia

1. Install GraphQL

npm install @nestjs/graphql @nestjs/apollo @apollo/server graphql
// app.module.ts
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      sortSchema: true,
      playground: process.env.NODE_ENV !== 'production',
      context: ({ req, res }) => ({ req, res }),
    }),
  ],
})
export class AppModule {}

2. Object Types (Code-first)

// users/models/user.model.ts
import { ObjectType, Field, ID, Int } from '@nestjs/graphql';

@ObjectType()
export class User {
  @Field(() => ID)
  id: string;

  @Field()
  email: string;

  @Field()
  name: string;

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

  @Field(() => Int)
  age: number;

  @Field(() => [Post], { nullable: 'items' })
  posts?: Post[];

  @Field()
  createdAt: Date;
}

@ObjectType()
export class Post {
  @Field(() => ID)
  id: string;

  @Field()
  title: string;

  @Field()
  content: string;

  @Field(() => User)
  author: User;
}

3. Resolvers

// users/users.resolver.ts
import { Resolver, Query, Mutation, Args, ResolveField, Parent } from '@nestjs/graphql';

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

  @Query(() => [User], { name: 'users' })
  findAll(
    @Args('limit', { type: () => Int, defaultValue: 10 }) limit: number,
    @Args('offset', { type: () => Int, defaultValue: 0 }) offset: number,
  ) {
    return this.usersService.findAll({ limit, offset });
  }

  @Query(() => User, { name: 'user' })
  findOne(@Args('id', { type: () => ID }) id: string) {
    return this.usersService.findById(id);
  }

  // ResolveField cho nested relation
  @ResolveField(() => [Post])
  posts(@Parent() user: User) {
    return this.postsService.findByAuthorId(user.id);
  }
}

4. Input Types and Mutations

// users/dto/create-user.input.ts
import { InputType, Field } from '@nestjs/graphql';
import { IsEmail, MinLength } from 'class-validator';

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

  @Field()
  @MinLength(2)
  name: string;

  @Field()
  @MinLength(8)
  password: string;
}

@InputType()
export class UpdateUserInput extends PartialType(CreateUserInput) {
  @Field(() => ID)
  id: string;
}
// Mutations trong resolver
@Mutation(() => User)
createUser(@Args('input') input: CreateUserInput) {
  return this.usersService.create(input);
}

@Mutation(() => User)
@UseGuards(GqlAuthGuard)
updateUser(
  @Args('input') input: UpdateUserInput,
  @CurrentUser() currentUser: User,
) {
  return this.usersService.update(input.id, input);
}

@Mutation(() => Boolean)
@UseGuards(GqlAuthGuard)
deleteUser(@Args('id', { type: () => ID }) id: string) {
  return this.usersService.delete(id);
}

5. Authentication for GraphQL

// auth/gql-auth.guard.ts
import { ExecutionContext, Injectable } 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;
  }
}

// Current user decorator cho GraphQL
export const CurrentUser = createParamDecorator(
  (data: unknown, context: ExecutionContext) => {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req.user;
  },
);

6. DataLoader — Solve N+1

npm install dataloader
// common/dataloader/users.loader.ts
import DataLoader from 'dataloader';

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

  readonly batchUsers = new DataLoader<string, User>(async (ids: string[]) => {
    const users = await this.usersService.findByIds([...ids]);
    const usersMap = new Map(users.map((u) => [u.id, u]));
    return ids.map((id) => usersMap.get(id));
  });
}

// Sử dụng trong resolver
@ResolveField(() => User)
author(@Parent() post: Post) {
  return this.usersLoader.batchUsers.load(post.authorId);
}

7. Subscriptions (Real-time)

// app.module.ts - Enable subscriptions
GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  autoSchemaFile: true,
  subscriptions: {
    'graphql-ws': true,
  },
})

// posts/posts.resolver.ts
import { PubSub } from 'graphql-subscriptions';

const pubSub = new PubSub();

@Resolver(() => Post)
export class PostsResolver {
  @Mutation(() => Post)
  async createPost(@Args('input') input: CreatePostInput) {
    const post = await this.postsService.create(input);
    pubSub.publish('postCreated', { postCreated: post });
    return post;
  }

  @Subscription(() => Post)
  postCreated() {
    return pubSub.asyncIterableIterator('postCreated');
  }
}

8. Summary

  • Code-first: Use decorators to automatically generate schema
  • Resolvers: Query, Mutation, Subscription, ResolveField
  • DataLoader: Batch queries, solve N+1 problem
  • Auth: GqlAuthGuard + CurrentUser decorator
  • Subscriptions: Real-time updates via WebSocket

The next article will explore File Upload, Caching and Task Scheduling.