Table of Contents
環境
- NestJS ^6.0.0
- Node.js v12.6.0
セットアップ
セットアップは 前回の記事 を参照してください。
GraphQL導入
$ npm i --save @nestjs/graphql apollo-server-express graphql-tools graphql
GraphQL開発手順
NestJSでのGraphQL開発手法は二つのアプローチがあります。
- スキーマファーストアプローチ
- コードファーストアプローチ
それぞれのアプローチを実際にコードを使って説明していきたいと思います。
スキーマファーストアプローチ
NestJSでは二つのアプローチからGraphQLアプリを開発することができ、一つはスキーマファーストなアプローチです。
まずGraphQLのスキーマファイルを定義して、そこからTypeScriptコードを生成(interface/class) して開発する手法です。
GraphQLModule
app.module.tsを編集してGraphQLモジュールを追加します。
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLModule.forRoot({
typePaths: ['./**/*.graphql'],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
GraphQLのスキーマファイルschema.graphqlを作成します。
# src/schema.graphql
type Query {
todos: [Todo!]!
}
type Todo {
id: ID!
title: String!
description: String
}
準備ができたのでアプリを立ち上げてみます。
$ npm run start
無事に立ち上がったら、Playgroundが使えるようになったので localhost:自分のポート番号/graphql
にアクセスして確認します。
また以下のような設定をするとGraphQLスキーマを元にTypeScriptファイルが自動生成されるようになります。
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path'; // <- 追加
@Module({
imports: [
GraphQLModule.forRoot({
typePaths: ['./**/*.graphql'],
definitions: { // <- 追加
path: join(process.cwd(), 'src/graphql.ts'),
},
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
変更後、アプリを再起動すると graphql.tsというファイルが自動生成されます。
// src/graphql.ts
/** ------------------------------------------------------
* THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
* -------------------------------------------------------
*/
/* tslint:disable */
export interface IQuery {
todos(): Todo[] | Promise<Todo[]>;
}
export interface Todo {
id: string;
title: string;
description?: string;
}
デフォルトはinterfaceで定義されますが、outputAs
オプションを class
と指定することclassで出力されるようになります。
GraphQLDefinitionsFactory
このままだとGraphQLスキーマ(schema.graphql)を変更する度に毎回アプリを再起動してtsファイルを更新しないといけないです。
@nestjs/graphql
の GraphQLDefinitionsFactory
を利用してスキーマの更新を検知してtsファイルを自動生成するよう設定を行います。
// src/generate-typings.ts
import { GraphQLDefinitionsFactory } from '@nestjs/graphql';
import { join } from 'path';
const definitionsFactory = new GraphQLDefinitionsFactory();
definitionsFactory.generate({
typePaths: ['./src/**/*.graphql'],
path: join(process.cwd(), 'src/graphql.ts'),
outputAs: 'class',
watch: true,
});
watch
オプションを指定することで.graphqlファイルの変更を検知してtsファイルを自動生成してくれます。
generate-typings.tsで出力設定を書いているため、app.module.tsではその記述はいらなくなります。
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLModule.forRoot({
typePaths: ['./src/**/*.graphql'],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
generate-typings.tsをnote-tsで動かすスクリプト設定を package.json に記述します。
{
.
"scripts": {
"generate-typings": "ts-node src/generate-typings.ts",
.
.
}
}
すると、以下コマンドで自動生成をwatchモードで動いてくれます。
$ npm run generate-typings
今回はclassで出力するように設定したので、以下のようになります。
// src/graphql.ts
/** ------------------------------------------------------
* THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
* -------------------------------------------------------
*/
/* tslint:disable */
export abstract class IQuery {
abstract todos(): Todo[] | Promise<Todo[]>;
}
export class Todo {
id: string;
title: string;
description?: string;
}
このように、スキーマファーストアプローチでは最初にスキーマを定義して、そこからコード(interface/class)が生成されます。そして、生成されたコードからビジネスロジックやDBアクセスを組み立てていく流れになります。
コードファースト
スキーマファーストとは逆にコードを先に書いていき、それを元にスキーマを生成するという開発アプローチになります。
このアプローチを実現するには type-graphql というライブラリが必要になりますのでインストールします。
$ npm install type-graphql
GraphQLModuleの設定も先ほどとは変わってきますので変更します。
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: './src/schema.graphql', // <- 変更
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
この設定でアプリ起動時に自分が書いたコードを元にGraphQLスキーマが自動生成されます。
モジュール
スキーマファーストアプローチで作成したスキーマと同じものを生成するように作っていきます。
まずTodoモジュールを用意します。
$ nest generate module todos
todos.module.ts ができます。
そして app.module.ts でimportします。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { TodosModule } from './todos/todos.module'; // <- 追加
@Module({
imports: [
TodosModule, // <- 追加
GraphQLModule.forRoot({
autoSchemaFile: './src/schema.graphql',
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
クラス
次にクラスを生成します。
$ nest generate class todos/todo
生成されたtodo.ts をtype-graphqlから自動生成されるようクラスにデコレータを付けます。
// src/todos/todo.ts
import { Field, ID, ObjectType } from 'type-graphql';
@ObjectType()
export class Todo {
@Field(type => ID)
id: string;
@Field()
title: string;
@Field({ nullable: true })
description?: string;
}
リゾルバ
そしてクエリのハンドリングを担うリゾルバを用意します。
$ nest generate resolver todos
Todoの一覧を返すようなクエリを用意します。
// src/todos/todos.resolver.ts
import { Resolver, Query } from '@nestjs/graphql';
import { Todo } from './todo';
const mockTodoService = {
getTodos() {
return Promise.resolve([
{
id: '1',
title: '読書',
description: '一時間本を読む',
},
{
id: '2',
title: 'マラソン',
description: '一時間マラソンする',
},
]);
},
};
@Resolver('Todos')
export class TodosResolver {
@Query(returns => [Todo])
todos(): Promise<Todo[]> {
return mockTodoService.getTodos();
}
}
本来はServiceクラスなどを用意しますが、省略のため簡単なモックを用意しています。
スキーマ生成
コード部分ができたので、あとはアプリを起動時にスキーマが自動生成されます。
$ npm run start
今回生成されるスキーマファイルは以下のようになります。
# src/schema.graphql
# -----------------------------------------------
# !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!!
# !!! DO NOT MODIFY THIS FILE BY YOURSELF !!!
# -----------------------------------------------
type Query {
todos: [Todo!]!
}
type Todo {
id: ID!
title: String!
description: String
}
コードファーストアプローチで最初に定義したものと同じになりましたね。
Playgroundで実行してみるとTodo一覧が取得できているのが確認できます。
まとめ
NestJSでのGraphQLアプリ開発は、
- スキーマを定義してコードが自動生成されるスキーマファーストアプローチ
- コードを書いてスキーマが自動生成されるコードファーストアプローチ
の二つのアプローチを紹介しました。
これらはGraphQLアプリ開発で起こる「スキーマとコードの同期」という冗長な部分を解決してくれます。
どのアプローチを取るかは、状況にはよると思いますが、
API仕様が決まっているプロジェクトの場合は、スキーマファーストアプローチを選ぶことができますし、
例えばすでにRESTアプリが作られていて、GraphQLへ移行する場合などはクラスが存在しているのでデコレータを付けてスキーマを生成するコードファーストなアプローチをすることができます。
Related Posts
Daiki Urata
2023/05/22
Daiki Urata
2023/01/05
Daiki Urata
2022/08/29