Making Code
Back to blog
Architecture3 min read

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.

CQRS in NestJS: Stop Mixing Reads and Writes in the Same Service

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