CQRS in NestJS: Stop Mixing Reads and Writes in the Same Service
When your UserService handles POST and GET, optimizations on one side break the other. Learn commands, queries, and handlers with @nestjs/cqrs.

There is a moment in every growing API when UserService does too much. It creates users, lists them with filters, exports CSVs, and sends admin alerts. You optimize a read query with a heavy join — and suddenly user registration times out.
CQRS (Command Query Responsibility Segregation) is not enterprise theatre. It is a discipline: writes and reads follow different paths, different handlers, and often different optimization strategies.
NestJS ships first-class support via @nestjs/cqrs. Here is how to use it without turning your app into a conference slide.
Commands change state; queries do not
| Commands | Queries | |
|---|---|---|
| HTTP | POST, PUT, DELETE | GET |
| Side effects | Yes | No |
| Returns | Domain entity / void | DTO / projection |
| Example | CreateUserCommand |
GetUsersPaginatedQuery |
If your "create user" handler calls findAll() to check something, that is a smell — split the logic.
The flow your team can draw on a whiteboard
POST /users → CommandBus → CreateUserHandler → UserRepositoryPort
GET /users → QueryBus → ListUsersHandler → UserRepositoryPort
Controllers hold zero business rules. They dispatch messages.
Your first command
export class CreateUserCommand extends Command {
constructor(
public readonly dto: { email: string; name: string; password: string },
) {
super();
}
}
The handler owns the rules
@CommandHandler(CreateUserCommand)
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
constructor(
@Inject(UserRepositoryPort) private readonly users: UserRepositoryPort,
) {}
async execute(command: CreateUserCommand): Promise<User> {
const exists = await this.users.findByEmail(command.dto.email);
if (exists) throw new ConflictException('Email already registered');
const password = await bcrypt.hash(command.dto.password, 10);
return this.users.create({ ...command.dto, password });
}
}
Return a domain shape, not a Swagger DTO. The controller maps to the API response if needed.
Your first query
@QueryHandler(GetUserByIdQuery)
export class GetUserByIdHandler implements IQueryHandler<GetUserByIdQuery> {
async execute(query: GetUserByIdQuery): Promise<UserResponseDto | null> {
const user = await this.users.findById(query.id);
return user ? UserResponseDto.fromEntity(user) : null;
}
}
Pagination, search, and sorting live in query handlers — that is where read models evolve.
Thin controller, happy team
@Controller('users')
export class UsersController {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
@Post()
create(@Body() dto: CreateUserDto) {
return this.commandBus.execute(new CreateUserCommand(dto));
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.queryBus.execute(new GetUserByIdQuery(id));
}
}
When CQRS is worth the folders
- Handlers exceed ~80 lines or mix read/write concerns.
- You add multiple entry points (HTTP + queue + CLI) reusing the same use cases.
- Read and write scale differently (reports vs writes).
For a five-endpoint CRUD demo, a thin service is fine. For a product API, CQRS pays back in testability and clarity.
Register handlers or suffer
Every handler must appear in providers of its module. Missing registration fails at runtime, not compile time — add a smoke test per module.
Closing thought
CQRS is not about microservices or Kafka on day one. It is about naming and separating the two things every API does: change data, and show data. NestJS gives you the buses; your job is to keep controllers dumb and handlers focused.
Get new posts by email
No spam — just a note when I publish something new on backend, cloud, and architecture.
One email when a post goes live. Unsubscribe anytime.
Related articles

How to Build a Multi-Tenant SaaS Application in NestJS Without Duplicating Your Code
If onboarding a new customer requires deploying a new application or duplicating an entire database, your SaaS architecture probably isn't ready to scale. Learn how to implement multi-tenancy in NestJS in a clean and maintainable way.

Your API Doesn't Need More Services, It Needs Events
If every new feature forces you to modify five different services, you probably have a coupling problem. Learn how Event-Driven Architecture helps decouple modules and scale NestJS applications.

Why Your NestJS Service Becomes a Mess (and How Hexagonal Architecture Fixes It)
Controllers that know too much, entities full of ORM decorators, and tests that need a database. A practical guide to ports and adapters in NestJS.