Nestjs-queryでGraphQL APIを爆速で立ち上げる
2021/09/20
Nestjs-query
というGraphQLエンドポイントをいい感じで用意してくれるライブラリを使って、GraphQL APIを爆速?で立ち上げたいと思います 😏
Nestjs-query
はORMと併用して使う必要がありますが、ここではTypeOrm
を採用したいと思います。
ORMはTypeOrm
以外にsequelize
, mongoose
, typegoose
が使用可能です。
この記事はNestjs-query公式サイトのExampleをベースに少しアレンジしたものになります。
Nestjs-query
に関してより詳細が知りたい方はそちらをご参考ください。
※ この記事の最終コードは以下のリポジトリにあげています。
https://github.com/highbridge326/nestjs-query-example
1. NestJSをset up
NestJS
のcliを使ってプロジェクトを作成します。
$ npm i -g @nestjs/cli
$ nest new nestjs-query-example
2. 依存パッケージをインストール
GraphQL APIを立ち上げる上で必要なパッケージを事前にインストールします。
Core
$ yarn add @nestjs-query/core @nestjs/common @nestjs/config apollo-server-express mysql2 class-transformer
graphql関連
$ yarn add @nestjs-query/query-graphql @nestjs/graphql graphql graphql-subscriptions class-validator dataloader
typeorm関連
$ yarn add @nestjs-query/query-typeorm @nestjs/typeorm typeorm
3. Moduleを作成
プロジェクト直下で以下のコマンドを実行し、Moduleを作成します。
$ nest g module todo-item
4. Entityを定義
Moduleと同様にプロジェクト直下で以下のコマンドを実行し、Entityファイルを作成します。
$ nest g class todo-item.entity todo-item
生成されたファイル内でTypeOrm
を使ってEntityを定義していきます。
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ファイルを作成。
$ nest g class todo-item.dto todo-item
DTOを定義します。
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に登録
作成したEntity、DTOをModuleに登録します。
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 {}
ここでのNestjsQueryGraphQLModule
がEntityとDTOをもとにいい感じにResolverを自動生成してくれます(生成されるGraphQLエンドポイントに関してはこの記事の最後にまとめています)。
7. DBのset up
最後に肝心のDBがまだset upできていませんので、docker-compose
を利用してDBのset upを行います。
一例として以下のdocker-compose.yml
を用意します。
(DBにはMySQL(MariaDB)
を採用しています。)
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
ファイルもプロジェクト直下に用意します。
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接続情報を記載して、すべての準備は完了です。
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の立ち上げ
docker-compose up -d
でDBを立ち上げた後
yarn start
でGraphQL APIが立ち上がります。
うまくいっていれば、http://localhost:3000/graphql
にアクセスするとgraphql playgroundが使えるようになっているはずです。
補足1:生成されるGraphQLエンドポイント
nestjs-query
によって生成されるエンドポイントは以下のとおりです。
findOne
{
todoItem(id: 1) {
id
title
completed
created
updated
}
}
findMany
{
todoItems(filter: { completed: { is: true } }) {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
node {
id
title
completed
created
updated
}
cursor
}
}
}
createOne
mutation {
createOneTodoItem(
input: { todoItem: { title: "Create One Todo Item", completed: false } }
) {
id
title
completed
created
updated
}
}
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
mutation {
updateOneTodoItem(input: { id: 3, update: { completed: false } }) {
id
title
completed
created
updated
}
}
updateMany
mutation {
updateManyTodoItems(
input: { filter: { id: { in: [1, 2] } }, update: { completed: true } }
) {
updatedCount
}
}
deleteOne
mutation {
deleteOneTodoItem(input: { id: 1 }) {
id
title
completed
created
updated
}
}
deleteMany
mutation {
deleteManyTodoItems(
input: { filter: { title: { like: "Create Many Todo Items%" } } }
) {
deletedCount
}
}
補足2:Relation
Entity同士にRelationがある場合はnestjs-query
でどう書けるか紹介します。
todo-item
に以下のようなsub-task
Entityが1対多で紐づく場合を考えてみます。
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;
}
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
, @ManyToOne
がtypeorm
でのRelationの定義になります。
次にこれまでの手順と同様にsub-task
のDTO、Moduleも以下の通り用意します。
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;
}
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-item
のDTOにも類似のデコレータを追記してRelationの設定は完了です。
@ObjectType('TodoItem')
@UnPagedRelation('subTasks', () => SubTaskDTO, { disableRemove: true })
※ Relation先が復数ある場合は@Relation
デコレータではなく@UnPagedRelation
などを使います。
おわり
これぐらいだったら爆速って言ってもいいよね...? 🤔