Skip to content
NestJS ns cqrs 4 min read

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 styleWhen to useTrade-off
Promise<void>Fire-and-forget writes; result fetched later via a queryPurest CQRS; needs a follow-up read
Lightweight result ({ id })REST endpoints needing the new resource idSlight coupling of read shape to write
Full entityInternal services where re-querying is wastefulBlurs 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 fields readonly.
  • Give each command exactly one handler; put all orchestration logic in execute.
  • Inject repositories and the EventBus into 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 void for strict CQRS.
  • Group handlers into an exported CommandHandlers array so module registration scales cleanly.
  • Throw domain-specific exceptions from handlers and translate them with exception filters at the edge.
Last updated June 14, 2026
Was this helpful?