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
NestJS は GraphQL の構築に2つの方法を提供しており,code first
とschema first
である.
これらは好きな方を選ぶことが可能であり,code first
はその名の通り,TypeScript クラスによって GraphQL スキーマを生成できる.対してschema first
は,GraphQL SDL (Schema Definition Language)ファイルから TypeScript のクラスやインタフェースが自動生成されるというものであるとのこと.
私はcode first
を普段から使っているため,今回はこちらで GraphQL を構築する.
REST API を GraphQL に移行するときの考え方
以前紹介した DB 接続された REST API の Module の模式図を再掲する.
ここで,REST API を GraphQL に移行するために考えるべきことは,Controller を GraphQL リゾルバに置き換えることである.
つまり,Module の構成としては以下のように変更を加えるだけで REST API を GraphQL に移行することができる.ここで,TodoController
とTodoService
のコードが分離されている事による恩恵を得られる.逆に,分離がうまく出来ていなければ,もうひと手間必要になってしまう事になる.
GraphQL の設定
まずはAppModule
にGraphQLModule
を設定する.
以前までのTypeOrmModule
やTodoModule
と並列して以下のように設定する.
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 と非常に似た開発が可能であり,上述した設定を施しつつ,特定のデコレータを用いて定義したクラスによって自動でスキーマが生成される.
具体的には,以下のように定義する.
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 用の DTO を準備
ここまで設定していきなり GraphQL のリゾルバを記述してもよいのだが,REST API で準備していた CreateTodo や UpdateTodo と同等の機能を実装するために,先に DTO を準備する事にした.
REST API における DTO は,シンプルなクラスで準備していたが,GraphQL における DTO は専用のデコレータを用いて実装する.これにより,GraphQL にどのような型の入力をするべきかをサーバへ認識させることができる.
REST API においてcreate-todo.dto.ts
だったものは以下のように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
も作成する.
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 における部分はリゾルバとなる.
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 に登録されていない.
リゾルバの登録
module
のcontrollers
の代わりにproviders
にTodoResolver
を入れる事により,GraphQL にリゾルバを登録することができる.
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 の一覧,スキーマ一覧を表示するボタンである.クエリ文を入力しながら,スキーマが分からなくなれば,こちらをクリックすることで確認が出来る.
上にタブがあるが,複数のクエリ文を保存しておき,あとから再度実行することも可能.
CreateTodo を実行してみる
左のクエリ入力部に以下を入力し,実行する.
mutation {
createTodo(
createTodoInput: { title: "sample todo", description: "on playground" }
) {
id
title
description
}
}
Todo を取得してみる
query {
todo(id: 7){
id
title
description
}
}
おわりに
todos()
やdeleteTodo()
も準備してあるので,ここまでついてきてくれた方には是非試してみてほしい!
また,@ResolveField()
を用いたサブフィールドの解決やsubscription
を用いたデータの購読等は,ここでは紹介しなかったが,公式ドキュメントを見てもらってもよいし,今後気が向いたら書いていこうと思う.