thumbnail

Nestjs-queryでGraphQL APIを爆速で立ち上げる

2021/09/20

Nestjs-queryというGraphQLエンドポイントをいい感じで用意してくれるライブラリを使って、GraphQL APIを爆速?で立ち上げたいと思います 😏 Nestjs-queryORMと併用して使う必要がありますが、ここではTypeOrmを採用したいと思います。 ORMTypeOrm以外にsequelize, mongoose, typegooseが使用可能です。 この記事はNestjs-query公式サイトのExampleをベースに少しアレンジしたものになります。 Nestjs-queryに関してより詳細が知りたい方はそちらをご参考ください。 ※ この記事の最終コードは以下のリポジトリにあげています。 https://github.com/highbridge326/nestjs-query-example

1. NestJSをset up

NestJSのcliを使ってプロジェクトを作成します。

shell
$ npm i -g @nestjs/cli
$ nest new nestjs-query-example

2. 依存パッケージをインストール

GraphQL APIを立ち上げる上で必要なパッケージを事前にインストールします。

Core

shell
$ yarn add @nestjs-query/core @nestjs/common @nestjs/config apollo-server-express mysql2 class-transformer

graphql関連

shell
$ yarn add @nestjs-query/query-graphql @nestjs/graphql graphql graphql-subscriptions class-validator dataloader

typeorm関連

shell
$ yarn add @nestjs-query/query-typeorm @nestjs/typeorm typeorm

3. Moduleを作成

プロジェクト直下で以下のコマンドを実行し、Moduleを作成します。

shell
$ nest g module todo-item

4. Entityを定義

Moduleと同様にプロジェクト直下で以下のコマンドを実行し、Entityファイルを作成します。

shell
$ nest g class todo-item.entity todo-item

生成されたファイル内でTypeOrmを使ってEntityを定義していきます。

todo-item/todo-item.entity.ts
import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity()
export class TodoItemEntity {
  @PrimaryGeneratedColumn()
  id!: string;

  @Column()
  title!: string;

  @Column()
  completed!: boolean;

  @CreateDateColumn()
  created!: Date;

  @UpdateDateColumn()
  updated!: Date;
}

5. DTOを定義

以下のコマンドでDTOファイルを作成。

shell
$ nest g class todo-item.dto todo-item

DTOを定義します。

todo-item/todo-item.dto.ts
import { FilterableField, IDField } from '@nestjs-query/query-graphql';
import { ObjectType, GraphQLISODateTime, Field, ID } from '@nestjs/graphql';

@ObjectType('TodoItem')
export class TodoItemDTO {
  @IDField(() => ID)
  id!: number;

  @FilterableField()
  title!: string;

  @FilterableField()
  completed!: boolean;

  @Field(() => GraphQLISODateTime)
  created!: Date;

  @Field(() => GraphQLISODateTime)
  updated!: Date;
}

6. Moduleに登録

作成したEntityDTOModuleに登録します。

todo-item/todo-item.module.ts
import { NestjsQueryGraphQLModule } from '@nestjs-query/query-graphql';
import { NestjsQueryTypeOrmModule } from '@nestjs-query/query-typeorm';
import { Module } from '@nestjs/common';
import { TodoItemDTO } from './todo-item.dto';
import { TodoItemEntity } from './todo-item.entity';

@Module({
  imports: [
    NestjsQueryGraphQLModule.forFeature({
      imports: [NestjsQueryTypeOrmModule.forFeature([TodoItemEntity])],
      resolvers: [{ DTOClass: TodoItemDTO, EntityClass: TodoItemEntity }],
    }),
  ],
})
export class TodoItemModule {}

ここでのNestjsQueryGraphQLModuleEntityDTOをもとにいい感じにResolverを自動生成してくれます(生成されるGraphQLエンドポイントに関してはこの記事の最後にまとめています)。

7. DBのset up

最後に肝心のDBがまだset upできていませんので、docker-composeを利用してDBのset upを行います。 一例として以下のdocker-compose.ymlを用意します。 (DBにはMySQL(MariaDB)を採用しています。)

docker-compose.yml
version: '3'

services:
  db:
    container_name: db
    image: mariadb
    restart: always
    volumes:
      - ./mariadb:/var/lib/mysql
    env_file: .env
    ports:
      - $DB_PORT:$DB_PORT

  adminer:
    container_name: adminer
    image: adminer
    restart: always
    ports:
      - 8080:8080

DB設定用の.envファイルもプロジェクト直下に用意します。

.env
DB_HOST=localhost
DB_PORT=3306
MYSQL_ROOT_PASSWORD=password
MYSQL_DATABASE=nestjs-query-example
MYSQL_USER=user
MYSQL_PASSWORD=password

最後にapp.module.tsにDB接続情報を記載して、すべての準備は完了です。

app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { join } from 'path';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodoItemModule } from './todo-item/todo-item.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: '.env',
    }),
    GraphQLModule.forRoot({
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        type: 'mysql',
        host: configService.get('DB_HOST'),
        port: configService.get<number>('DB_PORT'),
        username: configService.get('MYSQL_USER'),
        password: configService.get('MYSQL_PASSWORD'),
        database: configService.get('MYSQL_DATABASE'),
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        synchronize: true,
      }),
      inject: [ConfigService],
    }),
    TodoItemModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

8. GraphQL APIの立ち上げ

shell
docker-compose up -d

でDBを立ち上げた後

shell
yarn start

でGraphQL APIが立ち上がります。 うまくいっていれば、http://localhost:3000/graphqlにアクセスするとgraphql playgroundが使えるようになっているはずです。

補足1:生成されるGraphQLエンドポイント

nestjs-queryによって生成されるエンドポイントは以下のとおりです。

findOne

findOne
{
  todoItem(id: 1) {
    id
    title
    completed
    created
    updated
  }
}

findMany

findMany
{
  todoItems(filter: { completed: { is: true } }) {
    pageInfo {
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor
    }
    edges {
      node {
        id
        title
        completed
        created
        updated
      }
      cursor
    }
  }
}

createOne

createOne
mutation {
  createOneTodoItem(
    input: { todoItem: { title: "Create One Todo Item", completed: false } }
  ) {
    id
    title
    completed
    created
    updated
  }
}

createMany

createMany
mutation {
  createManyTodoItems(
    input: {
      todoItems: [
        { title: "Create Many Todo Items - 1", completed: false }
        { title: "Create Many Todo Items - 2", completed: true }
      ]
    }
  ) {
    id
    title
    completed
    created
    updated
  }
}

updateOne

updateOne
mutation {
  updateOneTodoItem(input: { id: 3, update: { completed: false } }) {
    id
    title
    completed
    created
    updated
  }
}

updateMany

updateMany
mutation {
  updateManyTodoItems(
    input: { filter: { id: { in: [1, 2] } }, update: { completed: true } }
  ) {
    updatedCount
  }
}

deleteOne

deleteOne
mutation {
  deleteOneTodoItem(input: { id: 1 }) {
    id
    title
    completed
    created
    updated
  }
}

deleteMany

deleteMany
mutation {
  deleteManyTodoItems(
    input: { filter: { title: { like: "Create Many Todo Items%" } } }
  ) {
    deletedCount
  }
}

補足2:Relation

Entity同士にRelationがある場合はnestjs-queryでどう書けるか紹介します。 todo-itemに以下のようなsub-taskEntityが1対多で紐づく場合を考えてみます。

sub-task.entity.ts
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  ObjectType,
  ManyToOne,
  JoinColumn,
} from 'typeorm';
import { TodoItemEntity } from '../todo-item/todo-item.entity';

@Entity()
export class SubTaskEntity {
  @PrimaryGeneratedColumn()
  id!: string;

  @Column()
  title!: string;

  @Column()
  completed!: boolean;

  @Column({ nullable: false })
  todoItemId!: string;

  @ManyToOne(
    (): ObjectType<TodoItemEntity> => TodoItemEntity,
    (td) => td.subTasks,
    { onDelete: 'CASCADE', nullable: false },
  )
  @JoinColumn()
  todoItem!: TodoItemEntity;

  @CreateDateColumn()
  created!: Date;

  @UpdateDateColumn()
  updated!: Date;
}
todo-item.entity.ts
import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
  OneToMany,
} from 'typeorm';
import { SubTaskEntity } from '../sub-task/sub-task.entity';

@Entity()
export class TodoItemEntity {
  @PrimaryGeneratedColumn()
  id!: string;

  @Column()
  title!: string;

  @Column()
  completed!: boolean;

  @OneToMany(() => SubTaskEntity, (subTask) => subTask.todoItem)
  subTasks!: SubTaskEntity[];

  @CreateDateColumn()
  created!: Date;

  @UpdateDateColumn()
  updated!: Date;
}

@OneToMany, @ManyToOnetypeormでのRelationの定義になります。

次にこれまでの手順と同様にsub-taskDTOModuleも以下の通り用意します。

sub-task.dto.ts
import {
  FilterableField,
  IDField,
  Relation,
} from '@nestjs-query/query-graphql';
import { ObjectType, ID, GraphQLISODateTime } from '@nestjs/graphql';
import { TodoItemDTO } from '../todo-item/todo-item.dto';

@ObjectType('SubTask')
@Relation('todoItem', () => TodoItemDTO, { disableRemove: true })
export class SubTaskDTO {
  @IDField(() => ID)
  id!: string;

  @FilterableField()
  title!: string;

  @FilterableField()
  completed!: boolean;

  @FilterableField(() => GraphQLISODateTime)
  created!: Date;

  @FilterableField(() => GraphQLISODateTime)
  updated!: Date;

  @FilterableField()
  todoItemId!: string;
}
sub-task.module.ts
import { NestjsQueryGraphQLModule } from '@nestjs-query/query-graphql';
import { NestjsQueryTypeOrmModule } from '@nestjs-query/query-typeorm';
import { Module } from '@nestjs/common';
import { SubTaskDTO } from './sub-task.dto';
import { SubTaskEntity } from './sub-task.entity';

@Module({
  imports: [
    NestjsQueryGraphQLModule.forFeature({
      imports: [NestjsQueryTypeOrmModule.forFeature([SubTaskEntity])],
      resolvers: [{ DTOClass: SubTaskDTO, EntityClass: SubTaskEntity }],
    }),
  ],
})
export class SubTaskModule {}

上記のDTOclassでは@Relationデコレータが追記されています。 nestjs-queryではこの@RelationデコレータをDTOに追記するだけでsub-taskからtodo-itemを呼び出すことができるようになります。

sub-taskと同様にtodo-itemDTOにも類似のデコレータを追記してRelationの設定は完了です。

todo-item.dto.ts
@ObjectType('TodoItem')
@UnPagedRelation('subTasks', () => SubTaskDTO, { disableRemove: true })

※ Relation先が復数ある場合は@Relationデコレータではなく@UnPagedRelationなどを使います。

おわり

これぐらいだったら爆速って言ってもいいよね...? 🤔

author picture

Mitsuru Takahashi

京都市内にてフリーランスエンジニアとして活動しています。

detail

Profile

author picture

Mitsuru Takahashi

京都市内にてフリーランスエンジニアとして活動しています。

detail

© 2022 mitsuru takahashi