Commands & Command Handlers
In a CQRS architecture every state change starts as a command — an explicit, intent-revealing object that says what should happen without saying how. A command names a single write operation (CreateUserCommand, TransferFundsCommand), carries the data that operation needs, and is dispatched onto the CommandBus. Exactly one command handler picks it up, performs the work, and optionally returns a result. This separation keeps your write side focused, testable, and easy to extend with middleware like logging or validation.
Defining a command
A command is a plain class. It carries no behaviour — only the payload required to execute the operation. Marking it with the ICommand interface from @nestjs/cqrs documents intent and lets handlers be strongly typed. Constructor parameter properties keep the definition compact.
// commands/create-user.command.ts
import { ICommand } from '@nestjs/cqrs';
export class CreateUserCommand implements ICommand {
constructor(
public readonly email: string,
public readonly displayName: string,
) {}
}
Use readonly fields so a dispatched command cannot be mutated mid-flight. Name commands as imperatives — they are instructions, not records of the past (that role belongs to events).
Implementing a command handler
A handler is an @Injectable-style class decorated with @CommandHandler(CommandClass) and implementing ICommandHandler<TCommand, TResult>. The execute method receives the command instance and runs the business logic. Because the class is registered as a provider, you can inject repositories, the EventBus, or any other service.
// commands/create-user.handler.ts
import { CommandHandler, ICommandHandler, EventBus } from '@nestjs/cqrs';
import { CreateUserCommand } from './create-user.command';
import { UserRepository } from '../user.repository';
import { UserCreatedEvent } from '../events/user-created.event';
export interface CreateUserResult {
id: string;
email: string;
}
@CommandHandler(CreateUserCommand)
export class CreateUserHandler
implements ICommandHandler<CreateUserCommand, CreateUserResult>
{
constructor(
private readonly users: UserRepository,
private readonly eventBus: EventBus,
) {}
async execute(command: CreateUserCommand): Promise<CreateUserResult> {
const { email, displayName } = command;
if (await this.users.existsByEmail(email)) {
throw new Error(`User with email ${email} already exists`);
}
const user = await this.users.save({ email, displayName });
this.eventBus.publish(new UserCreatedEvent(user.id, user.email));
return { id: user.id, email: user.email };
}
}
The handler returns a CreateUserResult. Whatever execute resolves to becomes the value returned from commandBus.execute, so callers can read the new id without a follow-up query.
Bind exactly one handler per command. If you register two handlers for the same command class, the last one wins silently — a common source of “my logic never runs” bugs.
Registering with the module
Both the CqrsModule and your handlers must be wired into a feature module. List every handler in providers. A common convention is to export an array of handlers so the module stays tidy as the set grows.
// user.module.ts
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { UserController } from './user.controller';
import { UserRepository } from './user.repository';
import { CreateUserHandler } from './commands/create-user.handler';
export const CommandHandlers = [CreateUserHandler];
@Module({
imports: [CqrsModule],
controllers: [UserController],
providers: [UserRepository, ...CommandHandlers],
})
export class UserModule {}
Dispatching through the CommandBus
Inject CommandBus wherever you need to trigger a write — typically a controller or an application service — and call execute. The bus locates the matching handler, awaits it, and returns the result. Keep controllers thin: their job is to build the command and hand it off.
// user.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { CreateUserCommand } from './commands/create-user.command';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UserController {
constructor(private readonly commandBus: CommandBus) {}
@Post()
async create(@Body() dto: CreateUserDto) {
const result = await this.commandBus.execute(
new CreateUserCommand(dto.email, dto.displayName),
);
return result;
}
}
execute is generically typed. When you pass a CreateUserCommand, TypeScript infers the return type as CreateUserResult, so result.id is fully typed with no casting.
Output:
$ curl -s -X POST http://localhost:3000/users \
-H 'content-type: application/json' \
-d '{"email":"[email protected]","displayName":"Ada"}'
{"id":"6f1c...","email":"[email protected]"}
Returning results vs. staying void
Strict CQRS says commands return nothing, but Nest lets handlers return a value, which is pragmatic for REST APIs that need the generated id back. Choose deliberately:
| Return style | When to use | Trade-off |
|---|---|---|
Promise<void> | Fire-and-forget writes; result fetched later via a query | Purest CQRS; needs a follow-up read |
Lightweight result ({ id }) | REST endpoints needing the new resource id | Slight coupling of read shape to write |
| Full entity | Internal services where re-querying is wasteful | Blurs command/query boundary |
Avoid returning rich read models from command handlers. If a screen needs detailed data after a write, route that through a query handler — it keeps the read and write sides independently optimisable.
Best Practices
- Name commands as imperatives (
ApproveOrderCommand) and keep their fieldsreadonly. - Give each command exactly one handler; put all orchestration logic in
execute. - Inject repositories and the
EventBusinto handlers rather than reaching for global state. - Validate input with DTOs and pipes at the controller boundary, before constructing the command.
- Return only the minimal result a caller needs (typically an id) — or
voidfor strict CQRS. - Group handlers into an exported
CommandHandlersarray so module registration scales cleanly. - Throw domain-specific exceptions from handlers and translate them with exception filters at the edge.