Fusic Tech Blog

Fusion of Society, IT and Culture

NestJSでのGraphQLアプリ開発手法
2019/09/29

NestJSでのGraphQLアプリ開発手法

本記事ではNestJSでのGraphQLアプリ開発手法について紹介します。

環境

  • 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スキーマを定義してPlaygroundを確認する

また以下のような設定をすると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/graphqlGraphQLDefinitionsFactory を利用してスキーマの更新を検知して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一覧が取得できているのが確認できます。

Playgroundでクエリを実行確認をする

まとめ

NestJSでのGraphQLアプリ開発は、

  • スキーマを定義してコードが自動生成されるスキーマファーストアプローチ
  • コードを書いてスキーマが自動生成されるコードファーストアプローチ

の二つのアプローチを紹介しました。

これらはGraphQLアプリ開発で起こる「スキーマとコードの同期」という冗長な部分を解決してくれます。

どのアプローチを取るかは、状況にはよると思いますが、

API仕様が決まっているプロジェクトの場合は、スキーマファーストアプローチを選ぶことができますし、

例えばすでにRESTアプリが作られていて、GraphQLへ移行する場合などはクラスが存在しているのでデコレータを付けてスキーマを生成するコードファーストなアプローチをすることができます。

Daiki Urata

Daiki Urata

フロントエンド好きなエンジニアです。