CQRS Introduction

Separate writes (commands) from reads (queries) with @nestjs/cqrs when a domain's complexity outgrows plain services.

CQRS - Command Query Responsibility Segregation - splits the model that changes state from the model that reads it. Nest ships @nestjs/cqrs to make the pattern first-class. It is not for every app, but for complex domains it tames sprawling services.

The three actors

  • Commands - intent to change state (CreateOrderCommand). Handled by exactly one CommandHandler.
  • Queries - requests to read (GetOrderQuery). Handled by one QueryHandler, often hitting a read-optimized store.
  • Events - facts that already happened (OrderCreatedEvent). Fan out to many EventHandlers for side effects.
export class CreateOrderCommand {
  constructor(public readonly items: string[]) {}
}

How it flows

A controller builds a command and dispatches it through the CommandBus. The matching handler executes the write, then publishes events. Read requests go through the QueryBus entirely separately. The buses decouple the controller from the handler - the controller never imports the handler directly.

When it earns its keep

CQRS adds files and indirection. It pays off when: writes and reads have very different shapes, you want an audit trail of events, or several side effects must react to one action. For simple CRUD it's overkill - a plain service is clearer.

The gateway to event sourcing

Because commands produce events, CQRS is the natural stepping stone to event sourcing, where those events are the source of truth. You don't have to go that far, but the pattern leaves the door open.

Example

Example · typescript
import { Injectable } from '@nestjs/common';
import {
  CommandHandler,
  ICommandHandler,
  EventBus,
  EventsHandler,
  IEventHandler,
  CommandBus,
} from '@nestjs/cqrs';

// ---- Command: the intent to write ----
export class CreateOrderCommand {
  constructor(
    public readonly customerId: string,
    public readonly items: string[],
  ) {}
}

// ---- Event: a fact that happened ----
export class OrderCreatedEvent {
  constructor(public readonly orderId: string) {}
}

@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler
  implements ICommandHandler<CreateOrderCommand>
{
  constructor(private readonly events: EventBus) {}

  async execute(cmd: CreateOrderCommand): Promise<{ orderId: string }> {
    const orderId = crypto.randomUUID();
    // ...persist the order here...
    this.events.publish(new OrderCreatedEvent(orderId)); // fan out
    return { orderId };
  }
}

// ---- One event, many independent reactions ----
@EventsHandler(OrderCreatedEvent)
export class SendConfirmationEmail
  implements IEventHandler<OrderCreatedEvent>
{
  handle(event: OrderCreatedEvent) {
    console.log(`emailing confirmation for ${event.orderId}`);
  }
}

// ---- The controller only knows the bus ----
@Injectable()
export class OrdersFacade {
  constructor(private readonly commandBus: CommandBus) {}
  place(customerId: string, items: string[]) {
    return this.commandBus.execute(
      new CreateOrderCommand(customerId, items),
    );
  }
}

When to use it

  • A developer uses @nestjs/cqrs to separate the PlaceOrderCommand handler (writes + side effects) from the GetOrderQuery handler (read-only) in a complex orders domain.
  • A team uses EventBus.publish(new OrderPlacedEvent(id)) inside a command handler so the billing and notification modules react to the event without direct coupling.
  • An engineer moves to CQRS when a plain service grew too large because order placement involved stock reservation, payment, email, and audit log steps all in one method.

More examples

Define a command and handler

Defines a CreateOrderCommand value object and its handler — the command carries the intent, the handler performs the write operation.

Example · ts
import { ICommand, CommandHandler, ICommandHandler } from '@nestjs/cqrs';

export class CreateOrderCommand implements ICommand {
  constructor(public readonly userId: number, public readonly items: string[]) {}
}

@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> {
  async execute(cmd: CreateOrderCommand) {
    // persist order, reserve stock, emit event...
    return { orderId: Date.now(), userId: cmd.userId };
  }
}

Dispatch command from controller

Injects CommandBus and dispatches the CreateOrderCommand from the controller, keeping business logic out of the HTTP layer.

Example · ts
import { Controller, Post, Body } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { CreateOrderCommand } from './commands/create-order.command';

@Controller('orders')
export class OrdersController {
  constructor(private readonly commandBus: CommandBus) {}

  @Post()
  async create(@Body() body: { userId: number; items: string[] }) {
    return this.commandBus.execute(
      new CreateOrderCommand(body.userId, body.items),
    );
  }
}

Query handler for reads

Implements a read-only query handler that fetches an order by ID, keeping the read model completely separate from the command model.

Example · ts
import { IQuery, IQueryHandler, QueryHandler } from '@nestjs/cqrs';

export class GetOrderByIdQuery implements IQuery {
  constructor(public readonly id: number) {}
}

@QueryHandler(GetOrderByIdQuery)
export class GetOrderByIdHandler implements IQueryHandler<GetOrderByIdQuery> {
  constructor(private readonly ordersRepo: OrdersRepository) {}

  execute(query: GetOrderByIdQuery) {
    return this.ordersRepo.findById(query.id);
  }
}

Discussion

  • Be the first to comment on this lesson.