【小学生でもわかる】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