🤗

【小学生でもわかる】NestJS CQRSのやさしい入門講座

に公開

こんにちは!今回は、NestJSでよく出てくる「CQRS(シーキューアールエス)」について、小学生にもわかるようなたとえ話と、実際の実装例を交えてやさしく解説します。

✅ 「なんか難しそう…」
✅ 「CQRSって本当に必要?」
✅ 「APIと関係あるの?」

そんな方に向けた記事です!


CQRSってなに?

CQRSとは、Command Query Responsibility Segregation(コマンド・クエリ責務分離)の略です。

でもそんな難しい言葉より、まずはたとえ話で理解してみましょう。


🏫 学校の「お願い係」と「記録係」

あなたの学校にこんな2人の先生がいたとします。

👩‍🏫 お願い係(Command)

  • 「給食のメニューを変えて!」
  • 「机を並べかえて!」
  • 「掃除当番を決めて!」

こんな感じで「何かを変えてほしい」というお願いを聞いてくれる先生です。

→ これが Command(コマンド)
→ 実際にお願いを実行する人を Command Handler(コマンドハンドラー) といいます

👨‍🏫 記録係(Query)

  • 「今日の給食はなに?」
  • 「掃除当番はだれ?」
  • 「机の並びはどうなってる?」

こういう「知りたいだけ」の質問に答えてくれる先生です。

→ これが Query(クエリ)
→ 実際に答える人を Query Handler(クエリハンドラー) といいます


✉️ コマンドとハンドラーの関係って?

📨 Commandくんに必要なものを投げると、勝手にHandlerさんが処理してくれるから、Controllerは責任放棄できる!

この表現が示すように、Controller(受付係)は「何をどう処理するか」を考える必要がありません。やることはただひとつ、命令(Command)を作って投げるだけ。あとはCommandHandler(実行係)がきちんと処理を担当してくれます。

この分業体制のおかげで、Controllerはとてもシンプルになり、役割が明確になります。

たとえば:

  • Command = 「お願いごとを書いた手紙」
  • CommandHandler = その手紙を読んで、ちゃんとやってくれる先生
  • Controller = 手紙を送る受付係(必要事項を受け取って渡すだけ)

✅ その構図をまとめると:

登場人物 役割 たとえ
Controller 外からの依頼を受け取る 教室の受付係(最低限しかやらない)
Command(命令くん) 「何をしてほしいか」だけを記述 手紙や依頼書
CommandBus(配達係) 手紙を届ける 配達員
Handler(実行係) 命令の内容を見て実行 実際に仕事する先生や担当者

🧾 実装構成の全体像

以下に、NestJSで @nestjs/cqrs を使って「予約の登録(Command)」と「予約の取得(Query)」を実現するシンプルなバックエンド構成一式を示します。

📁 ディレクトリ構成(サンプル)

src/
├── reservation/
│   ├── commands/
│   │   ├── create-reservation.command.ts
│   │   └── handlers/
│   │       └── create-reservation.handler.ts
│   ├── queries/
│   │   ├── get-reservations.query.ts
│   │   └── handlers/
│   │       └── get-reservations.handler.ts
│   ├── dto/
│   │   └── create-reservation.dto.ts
│   ├── reservation.controller.ts
│   ├── reservation.module.ts
│   ├── reservation.service.ts
└── main.ts

1. create-reservation.command.ts

export class CreateReservationCommand {
  constructor(
    public readonly userId: string,
    public readonly date: string,
  ) {}
}

2. create-reservation.handler.ts

import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreateReservationCommand } from '../create-reservation.command';
import { ReservationService } from '../../reservation.service';

@CommandHandler(CreateReservationCommand)
export class CreateReservationHandler
  implements ICommandHandler<CreateReservationCommand>
{
  constructor(private readonly reservationService: ReservationService) {}

  async execute(command: CreateReservationCommand) {
    return this.reservationService.create(command.userId, command.date);
  }
}

3. get-reservations.query.ts

export class GetReservationsQuery {
  constructor(public readonly userId: string) {}
}

4. get-reservations.handler.ts

import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { GetReservationsQuery } from '../get-reservations.query';
import { ReservationService } from '../../reservation.service';

@QueryHandler(GetReservationsQuery)
export class GetReservationsHandler
  implements IQueryHandler<GetReservationsQuery>
{
  constructor(private readonly reservationService: ReservationService) {}

  async execute(query: GetReservationsQuery) {
    return this.reservationService.findByUser(query.userId);
  }
}

5. create-reservation.dto.ts

export class CreateReservationDto {
  userId: string;
  date: string;
}

6. reservation.service.ts

import { Injectable } from '@nestjs/common';

interface Reservation {
  id: number;
  userId: string;
  date: string;
}

@Injectable()
export class ReservationService {
  private reservations: Reservation[] = [];
  private idCounter = 1;

  create(userId: string, date: string): Reservation {
    const reservation = { id: this.idCounter++, userId, date };
    this.reservations.push(reservation);
    return reservation;
  }

  findByUser(userId: string): Reservation[] {
    return this.reservations.filter(r => r.userId === userId);
  }
}

7. reservation.controller.ts

import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { CreateReservationDto } from './dto/create-reservation.dto';
import { CreateReservationCommand } from './commands/create-reservation.command';
import { GetReservationsQuery } from './queries/get-reservations.query';

@Controller('reservations')
export class ReservationController {
  constructor(
    private commandBus: CommandBus,
    private queryBus: QueryBus,
  ) {}

  @Post()
  async create(@Body() dto: CreateReservationDto) {
    return this.commandBus.execute(
      new CreateReservationCommand(dto.userId, dto.date),
    );
  }

  @Get()
  async find(@Query('userId') userId: string) {
    return this.queryBus.execute(new GetReservationsQuery(userId));
  }
}

8. reservation.module.ts

import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { ReservationService } from './reservation.service';
import { ReservationController } from './reservation.controller';
import { CreateReservationHandler } from './commands/handlers/create-reservation.handler';
import { GetReservationsHandler } from './queries/handlers/get-reservations.handler';

@Module({
  imports: [CqrsModule],
  controllers: [ReservationController],
  providers: [
    ReservationService,
    CreateReservationHandler,
    GetReservationsHandler,
  ],
})
export class ReservationModule {}

9. main.ts

import { NestFactory } from '@nestjs/core';
import { ReservationModule } from './reservation/reservation.module';

async function bootstrap() {
  const app = await NestFactory.create(ReservationModule);
  await app.listen(3000);
}
bootstrap();

👋 最後に

NestJSでCQRSを使うかどうかは、プロジェクトの大きさや複雑さ次第。
まずは普通の書き方で始めて、「読み書きがごちゃごちゃしてきたな」と思ったら、CQRSを検討するのがオススメです!

そのときは今回の「お願い係・記録係」のたとえ話を思い出してみてください😊

Discussion