NestJSでMySQLを利用したGraphQLなAPIを構築してみる
はじめに
NestJSでMySQLを利用したGraphQLなAPIを構築する際の備忘録になります。
と思ったら、手元のm1 macではDockerのmysqlイメージがarm64用がないようで利用できなかったため、mariaDBで代用しています。お試しの際は予めDockerを利用できる環境をご用意ください。
NestJSのインストール
NestJSとはTypeScriptで開発されたバックエンド向きのフルスタックフレームワークです。内部的にはExpressを使って処理を行っています。Fastifyをコアにさせることもできますが、GraphQLと若干相性が悪い?ようなので個人的にはもう少し様子見です。とにかくTypeScript全開のフレームワークで各種ORMやclass-validatorなどとも相性バツグンです😎
まずはNestJSのCLIをグローバルにインストールします。(このあたりはお好みで)
$ npm i -g @nestjs/cli
CLIを利用して新規プロジェクトを作成します。
今回はtodo-app
としてみました。
最初にパッケージマネージャとしてnpmかyarnを使うか聞かれますが今回はyarnで進めています。
$ nest new todo-app
? Which package manager would you ❤️ to use? yarn
✔ Installation in progress... ☕
🚀 Successfully created project todo-app
👉 Get started with the following commands:
$ cd todo-app
$ yarn run start
新規プロジェクトが終わると、実行コマンドが表示されますので、ディレクトリに移動して試しに起動してみましょう。
$ cd todo-app
$ yarn start
ブラウザで、http://localhost:3000
を開いて、お約束の画面が表示されればまずはセットアップ成功です。
DBの用意
今回はDBとしてmariaDBをDockerイメージから利用します。
GUIの管理ツールとしてadminerもついでに用意しておきます。
なお、テスト用とのためデータの永続化は行っていませんのでご注意ください。
version: '3.8'
services:
mysql:
image: mariadb:10.6.1
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MARIADB_ROOT_PASSWORD: example
ports:
- 3306:3306
adminer:
image: adminer
restart: always
ports:
- 8080:8080
設定ファイルを作成したらDockerを構築、起動します。
イメージが手元にない場合はダウンロード、構築に時間がかかります。
$ docker compose up
無事に起動したら動作をみておきましょう。
ブラウザでhttp://localhost:8080
に接続してadminerの管理画面を開きます。
データベース種類: MySQL
サーバ: mysql
ユーザ名: root
パスワード: example
データベース: 空白
でログインできればMySQLが利用可能です。
あわせて今回利用するデータベースtodo
を作成しておきます。
NestJSの初期設定
NestJSで.env
などの環境設定ファイルを利用するためのパッケージ@nestjs/config
をインスールしておきます。(すみません今回の記事内では結局利用しませんでした。。)
$ yarn add @nestjs/config
app.module.ts
ファイルに記載を追記することで、.env
ファイルに記載した情報をprocess.env
環境変数経由で取得可能です。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config'; // 追加
@Module({
imports: [ConfigModule.forRoot()], // 追加
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
TypeORMのインストール
TypeScript製ORMであるTypeORMをインストールします。
今回データベースにはmariaDBを利用しているので、mysql
パッケージもあわせてインストールします。
$ yarn add typeorm @nestjs/typeorm mysql
データベースの接続に必要になる設定ファイルを作成します。
synchronize
オプションはentities
ファイルを編集するごとにDBの構造を自動的に変更してくれますが、意図しない変更を防ぐためにも開発環境でのみtrue
とするようにしてください。
プロダクション利用を行うアプリケーションなどでは別途環境変数から設定を読み込む形にするのが理想かと思います。
{
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "root",
"password": "example",
"database": "todo",
"entities": ["dist/**/entities/*{.ts,.js}"],
"synchronize": true,
}
データベースへの接続
データベースに接続するためのModuleファイルsrc/database/database.module.ts
を作成します。
NestJSにはModuleやControllerなどをCLIで作成することができ、依存関係も自動的に追記してくれる上にテスト用のファイルまで作成してくれます。
│ name │ alias │ description │
│ application │ application │ Generate a new application workspace │
│ class │ cl │ Generate a new class │
│ configuration │ config │ Generate a CLI configuration file │
│ controller │ co │ Generate a controller declaration │
│ decorator │ d │ Generate a custom decorator │
│ filter │ f │ Generate a filter declaration │
│ gateway │ ga │ Generate a gateway declaration │
│ guard │ gu │ Generate a guard declaration │
│ interceptor │ in │ Generate an interceptor declaration │
│ interface │ interface │ Generate an interface │
│ middleware │ mi │ Generate a middleware declaration │
│ module │ mo │ Generate a module declaration │
│ pipe │ pi │ Generate a pipe declaration │
│ provider │ pr │ Generate a provider declaration │
│ resolver │ r │ Generate a GraphQL resolver declaration │
│ service │ s │ Generate a service declaration │
│ library │ lib │ Generate a new library within a monorepo │
│ sub-app │ app │ Generate a new application within a monorepo │
│ resource │ res │ Generate a new CRUD resource │
Moduleファイルを作成します。
$ nest g mo database
作成されたファイルを編集します。
import { Module } from '@nestjs/common';
import { Connection } from 'typeorm';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [TypeOrmModule.forRoot()],
})
export class DatabaseModule {
constructor(connection: Connection) {
if (connection.isConnected) {
console.log('DB connected!');
}
}
}
ここまで準備したところで開発サーバーを起動してDBへの接続ができているか試してみましょう。
$ yarn start:dev # :devをつけることでwatchモードで起動します
コンソールにDB connected!
が表示されていれば接続に成功しています。
なお、コンソールに下記のエラーが出てしまった場合は、adminerでtodoデータベースを作成できているか確認してみてください。
Error: ER_BAD_DB_ERROR: Unknown database 'todo'
GraphQLのセットアップ
GraphQLの利用に必要なパッケージをインストールします。
$ yarn add @nestjs/graphql graphql-tools graphql apollo-server-express
CLIでModule、Resolver、Serviceファイルを作成します。
$ nest g mo todos
$ nest g r todos
$ nest g s todos
一旦動作テストのためResolverに仮の処理を記載します。
import { Query, Resolver } from '@nestjs/graphql';
import { TodosService } from './todos.service';
@Resolver()
export class TodosResolver {
constructor(private todosService: TodosService) {}
@Query(() => String)
public async todos() {
return 'All todos';
}
}
作成したTodosModuleとインストールしたGraphQLModuleをapp.module.tsに読み込みます。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';
import { GraphQLModule } from '@nestjs/graphql';
import { TodosModule } from './todos/todos.module';
@Module({
imports: [
ConfigModule.forRoot(),
DatabaseModule,
GraphQLModule.forRoot({
playground: true,
debug: true,
// 下記に設定したファイル名でスキーマファイルが書き出されます
autoSchemaFile: 'schema.graphql',
}),
TodosModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
各ファイルの準備ができてところでhttp://localhost:3000/graphql
へアクセスしてGraphQL Playgroundが起動するか試してみましょう。
左側に下記クエリーを入力してAll todosというメッセージが返ってくれば接続成功です。
query {
todos
}
TODO Modelの作成
データベースに格納されるTODOのModelを作成していきます。
今回はEntityというディレクトリにModelを用意します。
今回は単純な項目として、下記を用意してみました。
- id 自動的に付与される文字列
- name 文字列
- priority 数値
- completed 真偽値
- createdAt 日時(自動更新)
- updatedAt 日時(自動更新)
利用できるデコレーター等はTypeORMのドキュメンを参照ください。
import { Field, ObjectType } from '@nestjs/graphql';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'todos' })
@ObjectType()
export class Todo {
@PrimaryGeneratedColumn('uuid')
@Field()
id: string;
@Column()
@Field()
name: string;
@Column()
@Field()
priority: number;
@Column()
@Field()
completed: boolean;
@CreateDateColumn()
@Field()
createdAt: Date;
@UpdateDateColumn()
@Field()
updatedAt: Date;
}
なお、開発サーバーを起動している状態でEntityファイルにModelデータを記載して編集、保存するたびに自動的にデータベースに反映がされていきます。都度マイグレーションの必要がないので効率よく開発を進められます。
Moduleファイル、Serviceファイルを編集して作成したTODO Entityを紐付けていきます。
import { Module } from '@nestjs/common';
import { TodosService } from './todos.service';
import { TodosResolver } from './todos.resolver';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Todo } from './entities/todo';
@Module({
imports: [TypeOrmModule.forFeature([Todo])],
providers: [TodosService, TodosResolver],
exports: [TodosService],
})
export class TodosModule {}
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Todo } from './entities/todo';
@Injectable()
export class TodosService {
constructor(
@InjectRepository(Todo) private todoRepository: Repository<Todo>,
) {}
public async getAllTodos(): Promise<Todo[]> {
const todos = await this.todoRepository.find({});
// Nest.jsにはエラーハンドリング用のクラスがありその1つを利用しています
//
if (!todos) throw new NotFoundException();
return todos;
}
}
Resolverを編集してServiceで設定したgetAllTodos
の実行結果を返すように変更します。
import { Query, Resolver } from '@nestjs/graphql';
import { Todo } from './entities/todo';
import { TodosService } from './todos.service';
@Resolver()
export class TodosResolver {
constructor(private todosService: TodosService) {}
@Query(() => [Todo])
public async todos(): Promise<Todo[]> {
return await this.todosService.getAllTodos().catch((err) => {
throw err;
});
}
}
ここまで記載したところで再度GraphQL Playgroundを利用して、動作を試してみましょう。
まだデータは空なので空の配列が返ってくれば成功です。
query {
todos {
id
}
}
クエリーの作成
一覧の取得ができたところで次はデータの書き込みMutationクエリーを作成していきます。
まずクエリーの型情報となるDTOを作成します。
import { Field, InputType, Int } from '@nestjs/graphql';
@InputType()
export class NewTodoInput {
@Field()
name: string;
@Field(() => Int)
priority: number;
@Field()
completed: boolean;
}
ServiceとResolverにTODOの追加ロジックを追加します。
import {
Injectable,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NewTodoInput } from './dto/new-todo.input';
import { Todo } from './entities/todo';
@Injectable()
export class TodosService {
constructor(
@InjectRepository(Todo) private todoRepository: Repository<Todo>,
) {}
public async getAllTodos(): Promise<Todo[]> {
const todos = await this.todoRepository.find({});
if (!todos) throw new NotFoundException();
return todos;
}
public async addTodo(newTodoData: NewTodoInput): Promise<Todo> {
const newTodo = this.todoRepository.create(newTodoData);
await this.todoRepository.save(newTodo).catch((err) => {
new InternalServerErrorException();
});
return newTodo;
}
}
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { NewTodoInput } from './dto/new-todo.input';
import { Todo } from './entities/todo';
import { TodosService } from './todos.service';
@Resolver()
export class TodosResolver {
constructor(private todosService: TodosService) {}
@Query(() => [Todo])
public async todos(): Promise<Todo[]> {
return await this.todosService.getAllTodos().catch((err) => {
throw err;
});
}
@Mutation(() => Todo)
public async addNewTodo(
@Args('newTodoData') newTodoData: NewTodoInput,
): Promise<Todo> {
return await this.todosService.addTodo(newTodoData).catch((err) => {
throw err;
});
}
}
それではデータが問題なく書き込めるか試してみます。
http://localhost:3000/graphql
を開いて、左側にMutationクエリーを書き込み実行します。
mutation {
addNewTodo(newTodoData: {
name: "牛乳を買ってくる",
priority: 1,
completed: false,
}) {
id
name
priority
completed
createdAt
updatedAt
}
}
結果投稿したデータにidとcreatedAt、updatedAtが自動的に追加されたデータが返ってくれば登録完了です。
念の為一覧の取得とadminerからデータベースの中身も確認してみます。
さいごに
これで一覧の取得とTODOの投稿ができるようになりました。
同じ用に更新や削除を追加していくことでGraphQLを利用した簡単なCRUD APIが完成します。
個人的にはMongoDBとの組み合わせが好みではありますが、自分の環境だとなかなかプロダクションとしての理解を得られないので通りやすいMySQL(mariaDB)を想定したものにしてみました😅
バリデーションや入力データの加工などまだまだ機能としては足りていないものばかりですが、
class-validator
、class-transformer
パッケージを利用することでデコレーターを利用して簡単に実装していくことが可能です。
$yarn add class-validator class-transformer
import { Field, InputType, Int } from '@nestjs/graphql';
import { IsString, Length } from 'class-validator';
@InputType()
export class NewTodoInput {
@Field()
@IsString() // 文字列に制限する
@Length(5, 140, { message: '5文字以上140文字以内です' }) // 最低5文字、最長140文字
name: string;
@Field(() => Int)
priority: number;
@Field()
completed: boolean;
}
main.ts
ファイルにGlobalPipesを追加
import { ValidationPipe } from '@nestjs/common'; // 追加
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe()); // 追加
app.enableCors();
// await app.listen(3000);
// 他のホストから参照したい場合は0.0.0.0を追記
await app.listen(3000, '0.0.0.0');
}
bootstrap();
おまけ
NestJSはデフォルトではExpressをコアとして動作しますが、より高速に動作するFastifyをコアとして動作させることもできます。apollo-server-fastifyパッケージがまだpreview版のためその点だけご注意ください。
$ yarn add @nestjs/platform-fastify apollo-server-fastify@3.0.0-preview.3
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
app.useGlobalPipes(new ValidationPipe());
app.enableCors();
// await app.listen(3000);
// 他のホストから参照したい場合は0.0.0.0を追記
await app.listen(3000, '0.0.0.0');
}
bootstrap();
# yarn start:dev
http://localhost:3000/graphql
にアクセスするとIDEとして最新版のApollo Sandboxが起動します。スキーマと連携してクエリの入力がかなり便利になっています。
Discussion