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

レッスン 15: NestJS を使用した GraphQL

NestJS を使用した GraphQL API。コードファーストアプローチ、リゾルバー、クエリ、ミューテーション、サブスクリプション、データローダー、認証。

💻 プログラミング — レッスン 15 レッスン 15: NestJS を使用した GraphQL

NestJS: 基本から高度まで

パート 4: 高度な機能

xdev.asia

1.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. オブジェクトタイプ (コードファースト)

// 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. リゾルバー

// 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. 入力の型と突然変異

// 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. 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 — 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. サブスクリプション (リアルタイム)

// 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. まとめ

  • コードファースト: デコレータを使用してスキーマを自動生成する
  • リゾルバー: クエリ、ミューテーション、サブスクリプション、ResolveField
  • データローダー: バッチクエリ、N+1 問題を解決
  • 認証: GqlAuthGuard + CurrentUser デコレータ
  • 定期購入: WebSocket経由のリアルタイム更新

次の記事で詳しく説明します ファイルのアップロード、キャッシュ、タスクのスケジュール設定。