4
Help us understand the problem. What are the problem?

posted at

updated at

GraphQL+NestJS+TypeORM+MySQLにHelloworldしてみた

はじめに

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

ブラウザで localhost:3000にアクセスします。
image.png

上記の表示がされれば動作確認完了です。

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 を次のように修正します。

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が自動的に生成されるようになります。

src/app.module.ts
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 が立ち上がります。

image.png

src/tasks/tasks.service.tsfindAll を次のように変更します。

src/tasks/tasks.service.ts
...
  findAll() {
    return [];
  }
...

次のようなクエリを打つと結果を取得することが出来ます(空配列が返ってきます)

query
{
  tasks{
    exampleField
  }
}

image.png

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つ作ります。

src/config/ormconfig.ts
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;
src/config/ormdatasource.ts
import { DataSource } from 'typeorm';
import ormconfig from './ormconfig';

export const AppDataSource = new DataSource(ormconfig);

.env を作ります

.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 にコマンドを追加します。

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につなぎこみます。
細かいコードの説明は割愛(もとい、説明できるほどわかってない)します。

src/tasks/entities/task.entity.ts
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;
}
src/tasks/dto/create-task.input.ts
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;
}
src/tasks/dto/update-task.input.ts
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
src/tasks/tasks.service.ts
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;
  }
}
src/tasks/tasks.module.ts
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 {}
src/app.module.ts
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からデータ操作の動作確認

新しく作成するには次のようなクエリを実行します。

query
mutation {
  createTask(createTaskInput:{title:"Hello", description:"Hello World!", status: NEW}) {
    id
    title
    description
    status
    createdAt
    updatedAt
  }
}

image.png

一覧を取得するには次のようなクエリを実行します。

query
{
  tasks{
    id
    title
    description
    status
    createdAt
    updatedAt
  }
}

image.png

データベースの中はこんな感じに格納されています。

image.png

まとめ

実際のところマイグレーション周りの設定とコマンドが一番時間かかりました。参考にした記事から色々とコードを拝借しています。先人の皆様に感謝します。

参考

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
4
Help us understand the problem. What are the problem?