CQRS Overview
CQRS — Command Query Responsibility Segregation — is the idea that the model you use to change state should be different from the model you use to read state. Most CRUD applications collapse both into a single service with mixed responsibilities, which works until the read and write paths start pulling in opposite directions. The @nestjs/cqrs module gives you a lightweight, in-process implementation of this pattern built around three buses, letting you separate intent from execution without dragging in a message broker. This page introduces the concepts and shows when the extra structure pays for itself.
What CQRS actually means
At its core CQRS splits every operation into one of two categories. A command expresses an intent to change something — CreateOrder, CancelSubscription, MarkAsShipped. A query asks a question and returns data without side effects — GetOrderById, ListActiveUsers. Crucially, commands and queries are messages: plain data objects describing what should happen, decoupled from the code that handles them.
This separation matters because reads and writes have genuinely different requirements. Writes need validation, business invariants, and transactional consistency. Reads need to be fast, may aggregate across several tables, and are often served from denormalized projections or a cache. By modeling them independently you can optimize each path on its own terms instead of forcing one repository to serve both masters.
CQRS is not the same as event sourcing. CQRS only separates the read and write models; event sourcing is a separate choice about how you persist write-side state. You can adopt CQRS with an ordinary relational database and never touch event sourcing.
The three buses
@nestjs/cqrs exposes three dispatchers. You inject the bus, hand it a message object, and the module routes it to the registered handler.
| Bus | Dispatches | Handler decorator | Returns |
|---|---|---|---|
CommandBus | Commands (state changes) | @CommandHandler() | A result, often void or an id |
QueryBus | Queries (reads) | @QueryHandler() | The requested data |
EventBus | Events (facts that happened) | @EventsHandler() | Nothing — fire and forget |
Commands and queries are routed to exactly one handler. Events are different: a single published event can be observed by many handlers, which is what makes the pattern good for decoupling side effects like sending email, updating a read projection, or emitting a webhook.
Installing and wiring the module
Install the package and import CqrsModule into the feature module that owns the slice of behaviour.
npm install @nestjs/cqrs
// orders/orders.module.ts
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { OrdersController } from './orders.controller';
import { CreateOrderHandler } from './commands/create-order.handler';
import { GetOrderHandler } from './queries/get-order.handler';
@Module({
imports: [CqrsModule],
controllers: [OrdersController],
providers: [CreateOrderHandler, GetOrderHandler],
})
export class OrdersModule {}
Each handler is just a provider, so it participates in normal dependency injection — you can inject repositories, the EventBus, or any other service into its constructor.
A command and query end to end
A command is a simple class carrying the data needed to perform the change. The controller’s only job is to translate an HTTP request into a message and dispatch it.
// orders/commands/create-order.command.ts
export class CreateOrderCommand {
constructor(
public readonly customerId: string,
public readonly sku: string,
public readonly quantity: number,
) {}
}
// orders/orders.controller.ts
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { CreateOrderCommand } from './commands/create-order.command';
import { GetOrderQuery } from './queries/get-order.query';
@Controller('orders')
export class OrdersController {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
@Post()
create(@Body() body: { customerId: string; sku: string; quantity: number }) {
return this.commandBus.execute(
new CreateOrderCommand(body.customerId, body.sku, body.quantity),
);
}
@Get(':id')
getOne(@Param('id') id: string) {
return this.queryBus.execute(new GetOrderQuery(id));
}
}
The controller has no idea how an order is created or read — it only knows the message contracts. Swapping the persistence strategy behind a handler never touches this file.
// orders/queries/get-order.handler.ts
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { GetOrderQuery } from './get-order.query';
@QueryHandler(GetOrderQuery)
export class GetOrderHandler implements IQueryHandler<GetOrderQuery> {
async execute(query: GetOrderQuery) {
// resolve from a read-optimized store / projection
return { id: query.id, status: 'PENDING' };
}
}
A request to POST /orders followed by GET /orders/123 produces output like this:
Output:
{"id":"123","status":"PENDING"}
When CQRS adds value — and when it does not
CQRS introduces real ceremony: more files, more indirection, and a mental model the whole team has to share. That cost is justified when:
- The domain has rich business rules and invariants on the write side that a flat service would muddle.
- Read and write workloads scale or change independently, or reads come from a different store than writes.
- You want side effects (notifications, projections, integrations) decoupled from the core transaction via events.
- Multiple teams or bounded contexts need clear, message-based contracts between slices.
For a straightforward CRUD admin panel, plain controllers and services are simpler and perfectly correct. Reach for CQRS when the write model has earned the complexity — not by default.
Best Practices
- Keep commands and queries as immutable, behaviour-free data objects; put logic in handlers, not messages.
- Never let a query mutate state — queries must be side-effect free so they stay cacheable and re-runnable.
- Give each handler exactly one responsibility; if it grows branches, that is usually a second command in disguise.
- Adopt CQRS per bounded context, not application-wide — mix it with ordinary services where the domain is simple.
- Publish domain events for cross-cutting side effects instead of calling other services directly from a command handler.
- Validate command input at the edge (DTOs + pipes) before it ever reaches the bus, so handlers can assume clean data.