ys memos

Blog

NestJS 入門コース5 GraphQL


api

2022/04/02

前記事


前々回,REST API を作成した.

今回は,REST API を GraphQL に移行しながら,その時のソースコードの変更点等を紹介していきたい.


NestJS のサーバに GraphQL を導入するためには,公式により提供される GraphQL 用のモジュールと,apollo 等の GraphQL パッケージが必要になる.公式のサンプルに置いてはmercuriusも挙げられているが,apollo を使うのが一番トラブルが少なく実装できるだろう.

$ npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-express

NestJS は GraphQL の構築に2つの方法を提供しており,code firstschema firstである.

これらは好きな方を選ぶことが可能であり,code firstはその名の通り,TypeScript クラスによって GraphQL スキーマを生成できる.対してschema firstは,GraphQL SDL (Schema Definition Language)ファイルから TypeScript のクラスやインタフェースが自動生成されるというものであるとのこと.

私はcode firstを普段から使っているため,今回はこちらで GraphQL を構築する.


以前紹介した DB 接続された REST API の Module の模式図を再掲する.

graph TB; id1(TodoController); id2(TodoService); id3(TodoRepository); id4(DB); subgraph   subgraph TodoModule id1-->id2-->id3; end id3-->id4; end

ここで,REST API を GraphQL に移行するために考えるべきことは,Controller を GraphQL リゾルバに置き換えることである.

つまり,Module の構成としては以下のように変更を加えるだけで REST API を GraphQL に移行することができる.ここで,TodoControllerTodoServiceのコードが分離されている事による恩恵を得られる.逆に,分離がうまく出来ていなければ,もうひと手間必要になってしまう事になる.

graph TB; id1(TodoResolver); id2(TodoService); id3(TodoRepository); id4(DB); subgraph   subgraph TodoModule id1-->id2-->id3; end id3-->id4; end

まずはAppModuleGraphQLModuleを設定する.

以前までのTypeOrmModuleTodoModuleと並列して以下のように設定する.

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 { Todo } from './todo/todo.entity';
import { TodoModule } from './todo/todo.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'database',
      entities: [Todo],
      synchronize: true,
    }),
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      // code-firstのための設定
      autoSchemaFile: true,
    }),
    TodoModule,
  ],
})
export class AppModule {}

code-firstのアプローチでは,TypeORM と非常に似た開発が可能であり,上述した設定を施しつつ,特定のデコレータを用いて定義したクラスによって自動でスキーマが生成される.

具体的には,以下のように定義する.

src/todo/todo.type.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';

// GraphQL用のスキーマを定義するためのデコレータ
// 引数に文字列を入れることができ,`@ObjectType('Todo')`とするとTodoという名前でスキーマを生成できる.
// TypeORMのEntityとクラス名が被らないようにしているため,好みに合わせて単にTodoでも問題ない
@ObjectType()
export class TodoType {
  // @Field()でスキーマのフィールドを定義
  // @Field()の引数はコールバックの戻り値で型を表現.このIntはGraphQLのInt
  @Field(() => Int)
  id: number;

  // このStringはGraphQLのString
  // [String]とすると,配列にできる
  @Field(() => String)
  title: string;

  @Field(() => String)
  description: string;
}

ここで定義したスキーマは自動で GraphQL に登録され,扱うことができる.


ここまで設定していきなり GraphQL のリゾルバを記述してもよいのだが,REST API で準備していた CreateTodo や UpdateTodo と同等の機能を実装するために,先に DTO を準備する事にした.

REST API における DTO は,シンプルなクラスで準備していたが,GraphQL における DTO は専用のデコレータを用いて実装する.これにより,GraphQL にどのような型の入力をするべきかをサーバへ認識させることができる.

REST API においてcreate-todo.dto.tsだったものは以下のようにcreate-todo.input.tsに対応させる.

src/todo/dto/create-todo.input.ts
import { Field, InputType } from '@nestjs/graphql';

// inputのスキーマを定義
@InputType()
export class CreateTodoInput {
  // inputのフィールドを定義
  @Field()
  title: string;

  @Field()
  description: string;
}

同様にupdate-todo.input.tsも作成する.

src/todo/dto/update-todo.input.ts
import { Field, InputType, Int } from '@nestjs/graphql';

@InputType()
export class UpdateTodoInput {
  // GraphQLのInt型のフィールド
  // sampleには
  @Field(() => Int)
  id: number;

  // Stringもコールバックの戻り値で指定可能
  // nullableオプションによって空の入力を許可する
  // [String]とすると,配列にできる
  @Field(() => String, { nullable: true })
  title?: string;

  @Field(() => String, { nullable: true })
  description?: string;
}

REST API におけるエンドポイントにあたる GraphQL における部分はリゾルバとなる.

src/todo/todo.resolver.ts
import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql';

import { TodoService } from './todo.service';
import { TodoType } from './todo.type';
import { CreateTodoInput } from './dto/create-todo.input';
import { UpdateTodoInput } from './dto/update-todo.input';

@Resolver()
export class TodoResolver {
  // 以前から使っているTodoServiceをそのまま流用.
  constructor(private readonly todoService: TodoService) {}

  // 変更を伴わないものはQueryで定義する
  // todo(id: 5)でidが5のTodoをTodoTypeで取得する
  // @Args('id')により,入力の`id`を用いる
  @Query(() => TodoType)
  todo(@Args('id') id: number): Promise<TodoType> {
    return this.todoService.findOne(id);
  }

  // todos(ids: [5, 6])でidが5,6のTodoをいっせいに取得
  // コールバックの戻り値を[TodoType]にしてあるが,これは配列を返す設定
  // @Args()に細かい設定がなされており,配列を入力するための設定である
  @Query(() => [TodoType])
  todos(
    @Args({ name: 'ids', type: () => [Int] }) ids: number[],
  ): Promise<TodoType[]> {
    return this.todoService.findMany(ids);
  }

  // 変更を伴うものはMutationで定義する
  // @Args('createTodoInput')で,上で定義したDTOで入力を受ける
  @Mutation(() => TodoType)
  createTodo(
    @Args('createTodoInput') createTodoInput: CreateTodoInput,
  ): Promise<TodoType> {
    return this.todoService.createTodo(createTodoInput);
  }

  @Mutation(() => TodoType)
  updateTodo(
    @Args('updateTodoInput') updateTodoInput: UpdateTodoInput,
  ): Promise<TodoType> {
    return this.todoService.updateTodo(updateTodoInput);
  }

  @Mutation(() => TodoType)
  deleteTodo(@Args('id') id: number): Promise<boolean> {
    return this.todoService.deleteTodo(id);
  }
}

ここまでで,リゾルバを設置したが,これはまだアプリの GraphQL に登録されていない.


modulecontrollersの代わりにprovidersTodoResolverを入れる事により,GraphQL にリゾルバを登録することができる.

src/todo/todo.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Todo } from './todo.entity';
import { TodoService } from './todo.service';
import { TodoResolver } from './todo.resolver';

@Module({
  imports: [TypeOrmModule.forFeature([Todo])],
  providers: [TodoService, TodoResolver],
})
export class TodoModule {}

NestJS における GraphQL は,GraphQL プレイグラウンド(以下,単にプレイグラウンドと呼ぶ)によって動作確認が可能である.そしてこれは標準では ON に設定されており,GraphQLModule.forRoot()playgroundフラグをfalseにしない限りはアクセス可能となっている.

プレイグラウンドによって,(1)登録されている GraphQL スキーマの確認,(2)GraphQL の Query, Mutation 等が確認,(3)GraphQL の実行および結果確認の三点を GUI(ブラウザ上)で扱うことができる.

プレイグラウンドは標準で,ブラウザを用いてhttp://localhost:3000/graphqlにアクセスすることで利用可能.

左半分はクエリを入力する部分,右半分はクエリの結果を確認する部分,真ん中のボタンをクリックか左半分の入力中にCtrl+Enterで実行できる.

右端に少しだけ見えてるDOCS/ SCHEMAはそれぞれ,Query や Mutation の一覧,スキーマ一覧を表示するボタンである.クエリ文を入力しながら,スキーマが分からなくなれば,こちらをクリックすることで確認が出来る.

上にタブがあるが,複数のクエリ文を保存しておき,あとから再度実行することも可能.

プレイグラウンドの画面


左のクエリ入力部に以下を入力し,実行する.

create-todo
mutation {
  createTodo(
    createTodoInput: { title: "sample todo", description: "on playground" }
  ) {
    id
    title
    description
  }
}

CreateTodoの実行


todo
query	{
  todo(id: 7){
    id
    title
    description
  }
}

todoの実行


todos()deleteTodo()も準備してあるので,ここまでついてきてくれた方には是非試してみてほしい!

また,@ResolveField()を用いたサブフィールドの解決やsubscriptionを用いたデータの購読等は,ここでは紹介しなかったが,公式ドキュメントを見てもらってもよいし,今後気が向いたら書いていこうと思う.


関連タグを探す