Nest.js + TypeORMでGraphQL

14 min read読了の目安(約12700字

Nest.jsとは

Nest.jsはNode.jsのフレームワークであり、TypeScriptを完全サポートしています。
デフォルトではExpressをコアとして動作しますが、Fastifyをコアとして動作させることもできます。

https://docs.nestjs.com/

Nest.jsと特徴は以下の通りです。

  • Angular風のアーキテクチャ
  • フレームワークでDIの機構が用意されている
  • デコレータによる関心の分離

Nest.jsプロジェクトの作成

最初に、Nest CLIをインストールします。
Nest CLIはAnguar CLIとよく似ておりアプリケーションの新規作成やservice、controller、modelなどのアプリケーションの構成要素の作成などを手助けしてくれます。

$ npm i -g @nestjs/cli

インストールが完了したら、nest newコマンドでプロジェクトを作成できます。

nest new nest-sample-app

プロジェクトディレクトリに移動して、start:devコマンドによって開発モードでアプリケーションを起動します。

cd nest-sample-app
npm run start:dev

http://localhost:3000 にアクセスすると、Hello World!と表示されているはずです。

GraphQLの導入

Nest.jsによるGraphQLの開発では、以下の2つの方法があります。

  • コードファースト
  • スキーマファースト

コードファーストのアプローチは、デコレートとTypeScriptのクラスを用いて作成します。GraphQLのスキーマ定義ファイルは自動的に作成されます。
反対にスキーマファーストのアプローチはGraphQKのスキーマ定義ファイルに基づいてTypeScript定義ファイルを自動的に作成します。

今回は、コードファーストのアプローチを採用します。

GraphQLのセットアップ

はじめに、パッケージをインストールします。

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

次に、app.module.tsファイルを編集しGraphQLModuleモジュールをインポートします。

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: [],
+ imports: [
+   GraphQLModule.forRoot({
+     autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
+   }),
+  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

GraphQLModuleモジュールの設定は、forRoot()メソッドから渡します。autoSchemaFileプロパティによってスキーマ定義ファイルのパスをしていします。
コードファーストアプローチを採用しているので、これは後ほど自動生成されることになります。

モジュールの作成

まずはモジュールを作成します。
以下コマンドによってbooksモジュールを作成します。

$ nest generate module books
CREATE src/books/books.module.ts (82 bytes)
UPDATE src/app.module.ts (496 bytes)

モデルの作成

続いてモデルの作成です。

$ nest generate class books/book
CREATE src/books/book.spec.ts (139 bytes)
CREATE src/books/book.ts (21 bytes)

作成したモデルにGraphQLのスキーマと対応させます。
モデルクラスにデコレータを付与してきます。

src/books/book.ts
import { Field, ID, ObjectType, Int } from '@nestjs/graphql';

@ObjectType()
export class Book {
  @Field((type) => ID)
  id: number;

  @Field()
  title: string;

  @Field()
  author: string;

  @Field((type) => Int)
  price: number;

  @Field()
  createdAt: Date;
}

リゾルーバの作成

次に、リゾルーバを作成します。
リゾルーバは、GraphQLのクエリ・ミューテーション・サブスクライブなどの操作を実際にどのように行うか実装します。

下記コマンドで、リゾルーバを作成します。

$ nest generate resolver books
CREATE src/books/books.resolver.spec.ts (463 bytes)
CREATE src/books/books.resolver.ts (87 bytes)
UPDATE src/books/books.module.ts (224 bytes)

リゾルーバの実装です。
実際の処理は後から作成するサービスの実装に任せます。サービスはコンストラクタインジェクションによって外部から注入します。

src/books/books.resolver.ts
import { NotFoundException } from '@nestjs/common';
import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Book } from './book';
import { BooksService } from './books.service';
import { newBookInput } from './dto/newBook.input';

@Resolver((of) => Book)
export class BooksResolver {
  constructor(private booksService: BooksService) {}

  @Query((returns) => [Book])
  books(): Promise<Book[]> {
    return this.booksService.findAll();
  }

  @Query((returns) => Book)
  async getBook(@Args({ name: 'id', type: () => Int }) id: number) {
    const book = await this.booksService.findOneById(id);
    if (!book) {
      throw new NotFoundException(id);
    }
    return book;
  }

  @Mutation((returns) => Book)
  addBook(@Args('newBook') newBook: newBookInput): Promise<Book> {
    return this.booksService.create(newBook);
  }

  @Mutation((returns) => Boolean)
  async removeBook(@Args({ name: 'id', type: () => Int }) id: number) {
    return this.booksService.remove(id);
  }
}

DTOの作成

NestJSのDTO(Data Transfer Object)はRequest Payload(body)の型定義を行うためのものです。型定義をすると同時に、バリデーションを含むことができます。

まずはリクエストバリデーションのために必要なパッケージをインストールします。

npm i class-validator class-transformer

src/booksフォルダ配下にdto/newBook.input.tsファイルを作成します。

src/books/dto/newBook.input.ts
import { Field, InputType, Int } from '@nestjs/graphql';
import { Max, MaxLength, Min } from 'class-validator';

@InputType()
export class NewBookInput {
 @Field()
 @MaxLength(30)
 title: string;

 @Field((type) => Int)
 @Min(0)
 @Max(9999)
 price: number;

 @Field((type) => [String])
 author: string;
}

@InputType()デコレータを付与することで、GraphQLのinput typesとして扱われます。
されに、class-validatorのデコレータによってバリデーションを定義しています。

https://github.com/typestack/class-validator

続いて、ValidationPipeを有効化します。
Nest.jsではPipesと呼ばれるデコレータによって入力を受け取る前(コントローラーやリゾルーバの処理に到達する前に)変換処理やバリデーション処理を行います。

ValidationPipeはNest.jsで予め利用できるビルドインパイプの一つです。

src/main.tsを編集します。

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
+ import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
+  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

サービスの作成

以下コマンドでサービスを作成します。

$ nest generate service books
CREATE src/books/books.service.spec.ts (453 bytes)
CREATE src/books/books.service.ts (89 bytes)
UPDATE src/books/books.module.ts (159 bytes)

サービスには実際のビジネスロジックを記述していきます。
ひとまずはモックの処理を実装しておきます。

src/books/books.service.ts
import { Injectable } from '@nestjs/common';
import { Book } from './book';
import { newBookInput } from './dto/newBook.input';

let books = [
  {
    id: 1,
    title: 'test 1',
    author: 'Joe',
    price: 1000,
    createdAt: new Date(),
  },
  {
    id: 2,
    title: 'test 2',
    author: 'Maria',
    price: 2000,
    createdAt: new Date(),
  },
  {
    id: 3,
    title: 'test 3',
    author: 'Smith',
    price: 3000,
    createdAt: new Date(),
  },
] as Book[];

@Injectable()
export class BooksService {
  findAll(): Promise<Book[]> {
    return Promise.resolve(books);
  }

  findOneById(id: number): Promise<Book> {
    const book = books.find((book) => book.id === id);
    return Promise.resolve(book);
  }

  create(data: newBookInput): Promise<Book> {
    const book: Book = {
      ...data,
      id: Date.now(),
      createdAt: new Date(),
    };
    books.push(book);

    return Promise.resolve(book);
  }

  async remove(id: number): Promise<boolean> {
    books = books.filter((book) => book.id !== id);
    return true;
  }
}

ここまでの実装を一旦確認してみましょう。
http://localhost:3000/graphql にアクセスするとGraphQLのプレイグラウンドが表示されます。ここで作成したGraphQLを自由に試すことができます。

booksクエリを試してみましょう。

TypeORMの導入

次に、モックで実装していた処理をデータベースによる実装に置き換えていきます。
データベースにはMySQLを、ORMにはTypeORMを採用します。

https://typeorm.io/#/

TypeORMとは、名前通りTypeScriptと親和性の高いORMでありデコレータを用いてモデルを表現します。

TypeORMのセットアップ

まずはパッケージのインストールです。

npm install --save @nestjs/typeorm typeorm mysql2

MySQLも使えるようにしておきます。ローカルにインストールしたりDocekrで環境構築などが必要です。
この例ではローカルでインストールされたMySQLを使用します。

$ brew install mysql # Homebrewでインストール
$ mysql --version # バージョンを確認
$ mysql  Ver 8.0.22 for osx10.14 on x86_64 (Homebrew)
$ mysql.server start --skip-grant-tables # パスワード無しでログイン
$ mysql -uroot # rootでログイン

参考:

https://qiita.com/fuwamaki/items/194c2a82bd6865f26045

データベースを作成しておきます。

mysql> create database nest_sample_app;
Query OK, 1 row affected (0.10 sec)

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';
import { join } from 'path';
import { BooksModule } from './books/books.module';
+ import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    GraphQLModule.forRoot({
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
    }),
+     TypeOrmModule.forRoot({
+       type: 'mysql',
+       host: 'localhost',
+       port: 3306,
+       username: 'root',
+       password: '',
+       database: 'nest_sample_app',
+       entities: [],
+       synchronize: true,
+     }),
    BooksModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

synchronizetrueとするとTypeORMは自動でマイグレーションを実行します。

モデルの作成

モデルはGraphQLの導入に作成したものに、TypeORMのデコレータを追加する形で作成します。
books/book.tsを編集します。

src/books/book.ts
import { Field, ID, Int, ObjectType } from '@nestjs/graphql';
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
} from 'typeorm';

@Entity()
@ObjectType()
export class Book {
  @PrimaryGeneratedColumn()
  @Field((type) => ID)
  id: number;

  @Column({ length: '30' })
  @Field()
  title: string;

  @Column()
  @Field((type) => [String])
  author: string;

  @Column({ type: 'int', unsigned: true })
  @Field((type) => Int)
  price: number;

  @CreateDateColumn()
  @Field()
  createdAt: Date;
}

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';
import { BooksModule } from './books/books.module';
import { TypeOrmModule } from '@nestjs/typeorm';
+ import { Book } from './books/book';

@Module({
  imports: [
    GraphQLModule.forRoot({
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
    }),
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: '',
      database: 'nest_sample_app',
-       entities: [],
+       entities: [Book],
      synchronize: true,
    }),
    BooksModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

さらに、books/books.module.tsも修正します。

src/books/books.module.ts
import { Module } from '@nestjs/common';
import { BooksService } from './books.service';
import { BooksResolver } from './books.resolver';
import { TypeOrmModule } from '@nestjs/typeorm';
+ import { Book } from './book';

@Module({
 +  imports: [TypeOrmModule.forFeature([Book])],
  providers: [BooksService, BooksResolver],
})
export class BooksModule {}

サービスの修正

モデルの準備が完了したので、サービスの処理を置き換えていきます。

src/books/book.service.ts
import { Injectable } from '@nestjs/common';
import { Book } from './book';
import { newBookInput } from './dto/newBook.input';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

@Injectable()
export class BooksService {
  constructor(
    @InjectRepository(Book)
    private booksRepostiory: Repository<Book>,
  ) {}

  findAll(): Promise<Book[]> {
    return this.booksRepostiory.find();
  }

  findOneById(id: number): Promise<Book> {
    return this.booksRepostiory.findOne(id);
  }

  async create(data: newBookInput): Promise<Book> {
    const book = this.booksRepostiory.create(data);
    await this.booksRepostiory.save(book);
    return book;
  }

  async remove(id: number): Promise<boolean> {
    const result = await this.booksRepostiory.delete(id);
    return result.affected > 0;
  }
}

TypeORMはレポジトリデザインパターンを使用することができます。
InjectRepositoryデコレータでbooksRepositoryをインジェクションします。

各メソッドをbooksRepositoryを使ったものに置き換えています。

それでは実際に動作しているか確認してみましょう。

http://localhost:3000/graphqlにアクセスして、GraphQLプレイグラウンドでaddBookミューテーションを実行します。

データベースを確認します。

mysql> use nest_sample_app;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+---------------------------+
| Tables_in_nest_sample_app |
+---------------------------+
| book                      |
+---------------------------+
1 row in set (0.01 sec)

nset_sample_appデータベース内にbookテーブルが作成されています。
テーブルのデータも確認します。


mysql> select * from book;
+----+-------+----------------------------+-------+--------+
| id | price | createdAt                  | title | author |
+----+-------+----------------------------+-------+--------+
|  2 |  2011 | 2021-03-13 13:52:27.807220 | test  | Alice  |
+----+-------+----------------------------+-------+--------+
1 row in set (0.03 sec)

先程GraphQLで追加したデータが保存されています。

GraphQLプレイグラウンドに戻り、booksクエリを実行しましょう。

正しくデータベースの値が取得できていることがわかります。

終わりに

Nest.jsとTypeORMを用いた簡単なGraphQLを紹介しました。
すべてのコードは以下から参照できます。

https://github.com/azukiazusa1/nest-sample-app