はじめに
GraphQL+NestJS+TypeORM+MySQLという構成でのアプリケーション開発の初期環境構築を試してみました。
MySQL以外は素人なので調べながらなんとか動く構成にしたという感じです。
誰かの参考になれば幸せです。
成果物はこちら
事前準備
Node.js
, yarn
, mysql
をインストールしてください(説明は割愛します)
実行時の環境は次のとおりです。
$ npm -v
8.8.0
$ yarn -v
1.22.4
$ mysql -V
mysql Ver 8.0.28 for macos12.2 on x86_64 (Homebrew)
Nest CLI のインストール
次のコマンドを実行して Nest CLI
をインストールします
$ npm install -g @nestjs/cli
プロジェクトの作成と初期動作確認
CLIで新しいNestのプロジェクトを作り、そのフォルダに移動します。
パッケージマネージャーは yarn
を選択します。
$ nest new nestjs-typeorm-ts-example
? Which package manager would you ❤️ to use?
npm
❯ yarn
pnpm
$ cd nestjs-typeorm-ts-example
まずはお約束の動作確認を行います。下記コマンドでnestサーバーを起動します。
$ yarn start:dev
上記の表示がされれば動作確認完了です。
GraphQLの導入
GraphQLに必要なパッケージを yarn
で追加します。
$ yarn add @nestjs/graphql @nestjs/apollo graphql apollo-server-express
$ yarn add class-validator class-transformer #Validator使わなければ無くても良いけど
リソースの作成
tasks
リソースをジェネレーターで作ります。
トランスポートレイヤーは GraphQL (code first)
で。
CRUD
エントリポイントも作っちゃいます。
$ nest g resource tasks
? What transport layer do you use?
REST API
❯ GraphQL (code first)
GraphQL (schema first)
Microservice (non-HTTP)
WebSockets
? Would you like to generate CRUD entry points? (Y/n) Y
CREATE src/tasks/tasks.module.ts (224 bytes)
CREATE src/tasks/tasks.resolver.spec.ts (525 bytes)
CREATE src/tasks/tasks.resolver.ts (1109 bytes)
CREATE src/tasks/tasks.service.spec.ts (453 bytes)
CREATE src/tasks/tasks.service.ts (625 bytes)
CREATE src/tasks/dto/create-task.input.ts (196 bytes)
CREATE src/tasks/dto/update-task.input.ts (243 bytes)
CREATE src/tasks/entities/task.entity.ts (187 bytes)
UPDATE src/app.module.ts (1163 bytes)
GraphQLのための設定
nest-cli.json
を次のように修正します。
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": [
{
"name": "@nestjs/graphql",
"options": {
"introspectComments": true
}
}
]
}
}
また、src/app.module.ts
を次のように修正。これでsrc/schema.gql
が自動的に生成されるようになります。
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TasksModule } from './tasks/tasks.module';
@Module({
imports: [
TasksModule,
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
debug: true,
playground: true,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
GraphQLの動作確認
ここまで設定して localhost:3000/graphql
にアクセスすると、 GraphQL Playground
が立ち上がります。
src/tasks/tasks.service.ts
の findAll
を次のように変更します。
...
findAll() {
return [];
}
...
次のようなクエリを打つと結果を取得することが出来ます(空配列が返ってきます)
{
tasks{
exampleField
}
}
TypeORMの導入
必要なパッケージを追加します。
$ yarn add @nestjs/typeorm typeorm mysql2 reflect-metadata
typeorm-extensionの導入
データベースの create や drop
も行いたいので typeorm-extension
も導入します。
https://github.com/tada5hi/typeorm-extension
$ yarn add -D typeorm-extension
設定ファイルの作成
設定ファイルを2つ作ります。
import { DataSourceOptions } from 'typeorm';
const ormconfig: DataSourceOptions = {
name: 'default',
type: 'mysql',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT, 10),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
synchronize: false,
logging: false,
connectTimeout: 30 * 1000,
entities: [process.cwd() + '/dist/**/entities/**/*.entity.js'],
migrations: [process.cwd() + '/dist/database/migrations/**/*.js'],
charset: 'utf8mb4_general_ci',
};
export default ormconfig;
import { DataSource } from 'typeorm';
import ormconfig from './ormconfig';
export const AppDataSource = new DataSource(ormconfig);
.env
を作ります
# DB
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=password
DB_DATABASE=nestjs-typeorm-ts-example
必要に応じてシェルで以下のコマンドを実行して環境変数を設定してください。ここで環境変数を設定してからnestサーバーをサイド起動するのが良いでしょう。
$ export $(cat .env | grep -v ^# | xargs)
コマンドの追加
package.json
にコマンドを追加します。
...
"scripts": {
...
"db:create": "ts-node ./node_modules/typeorm-extension/dist/cli/index.js -f ./src/config/ormconfig.ts db:create",
"db:drop": "ts-node ./node_modules/typeorm-extension/dist/cli/index.js -f ./src/config/ormconfig.ts db:drop",
},
...
データベース作成
以下のコマンドでデータベースを作成します。エラーが発生した際は .env
から環境変数を読み込んでいるか、設定が間違っていないかなどを確認してください。
$ yarn db:create
yarn run v1.22.4
$ ts-node ./node_modules/typeorm-extension/dist/cli/index.js -f ./src/config/ormconfig.ts db:create
query: SELECT VERSION() AS `version`
query: START TRANSACTION
query: SELECT * FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = 'nestjs-typeorm-ts-example' AND `TABLE_NAME` = 'typeorm_metadata'
query: COMMIT
✨ Done in 5.61s.
また、データベースを削除する場合は次のように実行します。
$ yarn db:drop
エンティティをORMにつなぎこむ
task
エンティティをORMにつなぎこみます。
細かいコードの説明は割愛(もとい、説明できるほどわかってない)します。
import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql';
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
} from 'typeorm';
export enum TaskStatus {
NEW,
IN_PROGRESS,
COMPLETE,
}
registerEnumType(TaskStatus, {
name: 'TaskStatus',
});
@Entity()
@ObjectType()
export class Task {
@PrimaryGeneratedColumn()
@Field(() => ID)
id: string;
@Column({ length: '255' })
@Field()
title: string;
@Column('text')
@Field({ nullable: true })
description: string;
@Column({
type: 'enum',
enum: TaskStatus,
default: TaskStatus.NEW,
})
@Field(() => TaskStatus)
status: TaskStatus;
@CreateDateColumn()
@Field()
createdAt: Date;
@CreateDateColumn()
@Field()
updatedAt: Date;
}
import { InputType, Field } from '@nestjs/graphql';
import { MaxLength } from 'class-validator';
import { TaskStatus } from '../entities/task.entity';
@InputType()
export class CreateTaskInput {
@MaxLength(255)
@Field()
title: string;
@Field()
description: string;
@Field(() => TaskStatus)
status: TaskStatus;
}
import { CreateTaskInput } from './create-task.input';
import { InputType, Field, Int, PartialType } from '@nestjs/graphql';
@InputType()
export class UpdateTaskInput extends PartialType(CreateTaskInput) {
@Field(() => Int)
id: number;
}
マイグレーションを作ります。
$ npx ts-node ./node_modules/.bin/typeorm migration:generate src/database/migrations/create-task -d src/config/ormdatasource
マイグレーションを実行します。
$ npx ts-node ./node_modules/.bin/typeorm migration:run -d src/config/ormdatasource
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateTaskInput } from './dto/create-task.input';
import { UpdateTaskInput } from './dto/update-task.input';
import { Task } from './entities/task.entity';
@Injectable()
export class TasksService {
constructor(
@InjectRepository(Task)
private taskRepostiory: Repository<Task>,
) {}
async create(createTaskInput: CreateTaskInput) {
const task = this.taskRepostiory.create(createTaskInput);
await this.taskRepostiory.save(task);
return task;
}
findAll() {
return this.taskRepostiory.find();
}
async findOne(id: number) {
return await this.taskRepostiory.findOne({
where: {
id,
},
});
}
async update(id: number, updateTaskInput: UpdateTaskInput) {
const task = this.findOne(id);
if (task) {
await this.taskRepostiory.save(updateTaskInput);
}
}
async remove(id: number) {
const result = await this.taskRepostiory.delete(id);
return result.affected > 0;
}
}
import { Module } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { TasksResolver } from './tasks.resolver';
import { Task } from './entities/task.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [TypeOrmModule.forFeature([Task])],
exports: [TypeOrmModule],
providers: [TasksResolver, TasksService],
})
export class TasksModule {}
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import ormconfig from './config/ormconfig';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TasksModule } from './tasks/tasks.module';
@Module({
imports: [
TypeOrmModule.forRoot(ormconfig),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
debug: true,
playground: true,
}),
TasksModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
ここまででGraphQLからデータを操作してMySQLに格納したり取り出したりできるようになりました。
GraphQLからデータ操作の動作確認
新しく作成するには次のようなクエリを実行します。
mutation {
createTask(createTaskInput:{title:"Hello", description:"Hello World!", status: NEW}) {
id
title
description
status
createdAt
updatedAt
}
}
一覧を取得するには次のようなクエリを実行します。
{
tasks{
id
title
description
status
createdAt
updatedAt
}
}
データベースの中はこんな感じに格納されています。
まとめ
実際のところマイグレーション周りの設定とコマンドが一番時間かかりました。参考にした記事から色々とコードを拝借しています。先人の皆様に感謝します。
参考
- https://zenn.dev/naonao70/articles/a91d8835f1832b
- https://zenn.dev/msksgm/articles/20211107-typeorm-ormconfig
- https://qiita.com/potato4d/items/64a1f518abdfe281ce01
- https://zenn.dev/hakushun/articles/7daac74ae9af25
- https://zenn.dev/azukiazusa/articles/e84be9735d357e
- https://www.kindacode.com/snippet/using-enum-type-in-typeorm/
- https://www.wakuwakubank.com/posts/729-typeorm-migration/