Nest.js + TypeORMでGraphQL
Nest.jsとは
Nest.js は Node.js のフレームワークであり、TypeScript を完全サポートしています。
デフォルトでは Express をコアとして動作しますが、Fastify をコアとして動作させることもできます。
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
モジュールをインポートします。
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 のスキーマと対応させます。
モデルクラスにデコレータを付与してきます。
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)
リゾルーバの実装です。
実際の処理は後から作成するサービスの実装に任せます。サービスはコンストラクタインジェクションによって外部から注入します。
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
ファイルを作成します。
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
のデコレータによってバリデーションを定義しています。
続いて、ValidationPipe
を有効化します。
Nest.js ではPipesと呼ばれるデコレータによって入力を受け取る前(コントローラーやリゾルーバの処理に到達する前に)変換処理やバリデーション処理を行います。
ValidationPipe
は Nest.js であらかじめ利用できるビルドインパイプの 1 つです。
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)
サービスには実際のビジネスロジックを記述します。
ひとまずはモックの処理を実装しておきます。
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 を採用します。
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でログイン
参考:
データベースを作成しておきます。
mysql> create database nest_sample_app;
Query OK, 1 row affected (0.10 sec)
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 {}
synchronize
を true
とすると TypeORM は自動でマイグレーションを実行します。
モデルの作成
モデルは GraphQL の導入に作成したものに、TypeORM のデコレータを追加する形で作成します。
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;
}
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
も修正します。
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 {}
サービスの修正
モデルの準備が完了したので、サービスの処理を置き換えていきます。
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 を紹介しました。
すべてのコードは以下から参照できます。
Discussion
book.service.tsbooks.service.ts
ご指摘ありがとうございます!修正しました!