🐡

Nest.js + TypeORMでGraphQL

2021/03/13に公開
2

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 であらかじめ利用できるビルドインパイプの 1 つです。

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/books.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

GitHubで編集を提案

Discussion