NestJSでGraphQLサーバを実装する

2021/12/30に公開

はじめに

今、参画してるプロジェクトでGraphQLを使用しているので勉強がてらGraphQLサーバを立ててみよう、という記事です。フロントエンドエンジニアなので実業務でサーバの実装はしないんですが、BFFサーバは自分で実装できるようになっておきたいな、という気持ちで取り組みます。

サーバサイドのフレームワークとしてはNestJSを使用します!TypeScriptフレンドリーでドキュメントを参考に実装していくと自然と読みやすいコードになっていくので、オススメです!

ありがたいことに以前書いたNestJSの記事が多くの方に読まれているようだったので、需要があると信じて書いていきます。

対象読者

  • NestJS使ってみたい方
  • GraphQLサーバを建ててみたい方
  • NestJSでGraphQLサーバ建てたかったんだよって方

実装環境

  • Node.js: v16.1.0
  • @nestjs/core: v8.0.0
  • graphql: v15.8.0
  • apollo-server-express: v3.5.0

実際のコード

記事中のコードはすべてGitHubで公開してるので気になる方はコチラを御覧ください。

フロントも実装していて、一応動くものにはなってます。(フロントエンドエンジニアなのにフロントの実装に手を抜いていて自分でもこれでいいのかと思っていますが、どうかお許しください。)

https://github.com/hakushun/dm_graphql-apollo

実装開始

プロジェクトのセットアップ

では早速手を動かしていきましょう!

チュートリアルでおなじみTodo Listを題材にCode Firstアプローチと言う手法の沿って実装していきます。

まずはNestJSのプロジェクトを作成します。

コチラを参考に下記コマンドです。

# すでにインストール済みの方は不要
npm i -g @nestjs/cli
# プロジェクト名はご自由に
nest new project-name
# プロジェクトのディレクトリに移動
cd project-name
# 動作確認
npm run start:dev

GraphQLとApolloの準備

続いて、GraphQLとApolloの依存関係をインストールします。

コチラを参考に下記コマンドです。

npm i @nestjs/graphql graphql@^15 apollo-server-express

インストールが完了したらサーバに接続します。

src/schema.gqlは後ほど作成されるファイルなので現時点でなくても先へ進みます。

// src/app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    GraphQLModule.forRoot({
      // schemaファイルのパスを指定
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      // 生成されたschemaを自動でsortされるためのオプションをオンにする
      sortSchema: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})

Todo Listの準備

GraphQLの準備が整ったら次はTodo Listの準備です。

@nest/cligenerateコマンドで関連ファイルを作成します。

# g は generate のエイリアス
# src/todo/todo.module.tsの作成
nest g module todo
# src/todo/todo.service.tsの作成
nest g service todo
# src/todo/todo.resolver.tsの作成
nest g resolver todo

上記コマンドでsrc/app.module.tsTodoModuleのインポートが追加されてるのがわかると思います。generateコマンドを使用することによってそこら辺もよしなにやってくれます。

続いてはTodoのModelを定義します。

ここでは最低限の内容を定義しました。

// src/todo/models/todo.models.ts
import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql';

export enum TodoStatus {
  NEW,
  IN_PROGRESS,
  COMPLETE,
}
// enumを使用する際は registerEnumType でenumを登録しなくてはならない
// https://docs.nestjs.com/graphql/unions-and-enums#enums
registerEnumType(TodoStatus, {
  name: 'TodoStatus',
});

// ObjectTypeデコレータを使用することで、定義したmodelを元にschemaが自動生成される
@ObjectType()
export class Todo {
  // schame上、ID型にしたいため、ReturnTypeFuncを引数に与える
  // ReturnTypeFuncを引数に与えない場合、idの型はString型になる
  @Field((type) => ID)
  id: string;

  // ここはString型で良いのでReturnTypeFuncを引数に与えない
  @Field()
  title: string;

  // nullを許容するためオプションを指定
  // オプションを指定しない限り、nullは許容されない(String!型になる)
  @Field({ nullable: true })
  description: string;

  // GraphQLに存在しない型(TodoStatus)を指定する場合は、ReturnTypeFuncを引数に与える
  @Field((type) => TodoStatus)
  status: TodoStatus;

  @Field()
  createdAt: Date;

  @Field()
  updatedAt: Date;
}

冒頭でCode Firstアプローチで実装していく、とお話しましたがまさにこの部分がそれに当たります。

自らschemaを定義しなくても、classで定義した上記コードをベースにしてschemaを生成してくれるのです。普段TypeScriptを書いてる人にとってはshemaを楽に作成できるのではないでしょうか。

schemaを自動生成させるために、serviceとresolverの実装も終わらせちゃいましょう。

// src/todo/todo.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { Todo } from './models/todo.models';

@Injectable()
export class TodoService {
  // 今回はDBと接続しないのでメモリ上にTodoを保存します。
  private todos: Todo[] = [];

  // 全件取得のメソッド
  findAll(): Todo[] {
    return this.todos;
  }
  // idを元に一件取得のメソッド
  findOneById(id: string): Todo {
    const result = this.todos.find((todo) => id === todo.id);
    if (!result) {
      // なかったら404エラーを返す。ビルトインのエラーも豊富にあってエラー処理も結構楽
      // https://docs.nestjs.com/exception-filters#built-in-http-exceptions
      throw new NotFoundException();
    }
    return result;
  }
}
// src/todo/todo.resolver.ts
import { Args, ID, Query, Resolver } from '@nestjs/graphql';
import { Todo } from './models/todo.models';
import { TodoService } from './todo.service';

// Resolverデコレータでresolverを定義
// https://docs.nestjs.com/graphql/resolvers#code-first-resolver
@Resolver()
export class TodoResolver {
  constructor(private todoService: TodoService) {}
  // QueryデコレータでQueryを定義
  // 第一引数にReturnTypeFuncを指定し、型を定義。ここではTodoの配列を指定。
  // 第二引数にオプションとして{ nullable: 'items' }を与えることでから配列を許容する。[Todo]!と同義。
  // デフォルトでは [Todo!]! になる。
  @Query(() => [Todo], { nullable: 'items' })
  findAll() {
    return this.todoService.findAll();
  }

  @Query(() => Todo)
  // Queryに引数がある場合はArgsデコレータで定義。
  // 第一引数に引数の名前、第二引数に型を指定。
  // schema上の型定義は findOneById(id: ID!): Todo! となる
  findOneById(@Args('id', { type: () => ID }) id: string) {
    return this.todoService.findOneById(id);
  }
}

上記ファイルを作成して保存すると、あら不思議。src/schema.gqlが自動で生成されていませんか?(されていない方はローカルサーバが起動してるか確かめて再度保存してみてください。)

ここで思い出してほしいんですが、src/app.module.tsの下記部分。

// src/app.module.ts 抜粋
    GraphQLModule.forRoot({
      // schemaファイルのパスを指定
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      // 生成されたschemaを自動でsortされるためのオプションをオンにする
      sortSchema: true,
    }),

autoShcemaFileに指定したのは、この自動生成されるschemaのパスだったわけです。

ここまでできたら http://localhost:3000/graphql にアクセスしてみてください。GraphQLのPlaygroundが立ち上がると思います。

適当なQueryを実行してみて、値が返ってくるか確認してみてください。

# Playground左側に記入し、真ん中の三角ボタンをクリック
{
  findAll{
    id
  }
}
{
  "data": {
    "findAll": []
  }
}

空配列が返ってきましたか?src/todo/todo.service.tsprivate todos: Todo[] = [];で実装した部分が空配列になってるためです。この配列を変更して再度Queryを実行すれば結果も変わってきます。試してみてください。

このあとの実装

ここまでできればあとは拡張していくのみです!Mutaionを定義してTodoを作成できるようにしたり、Statusを更新できるようにしたり、削除できるようにしたり、色々実装してみてください!

GitHubに上げたコードでは上記内容はできるようになってるのでうまく行かなかった方はそちらも参考にしてみてください。

個人的にはPrismaを使用してサーバメモリ上じゃなくてDBに保存できるようにしようかなと思ってます。みなさんもぜひいろいろ試して遊んでみてください!

おわりに

最後まで読んでいただきありがとうございます。

みなさん、いかがでしたでしょうか。NestJSでGraphQLサーバを建てれるようになってもらえたなら幸いです。

そして、NestJSの良さやGraphQLの楽しさが伝わればこの記事を書く意味はあったかなと思います。

マイクロサービスアーキテクチャが増えて、BFFの必要性が増し、今後ますますGraphQLの需要は高まっていくのではないでしょうか。そんな時フロントエンドエンジニアとしてもその実装に貢献できるようになっていたいなと思いました。

良いお年をお迎えください!

Discussion