CQRS en NestJS: deja de mezclar lecturas y escrituras en el mismo servicio
Cuando tu UserService atiende POST y GET, optimizar un lado rompe el otro. Comandos, consultas y handlers con @nestjs/cqrs, sin humo.

Llega un momento en toda API en crecimiento en el que UserService hace demasiado. Crea usuarios, los lista con filtros, exporta CSV y manda alertas. Optimizas una consulta con un join pesado — y el registro de usuarios empieza a hacer timeout.
CQRS (Command Query Responsibility Segregation) no es teatro enterprise. Es una disciplina: las escrituras y las lecturas siguen caminos distintos, handlers distintos y, a menudo, estrategias de optimización distintas.
NestJS lo soporta de primera con @nestjs/cqrs. Así se usa sin convertir la app en una diapositiva de conferencia.
Los comandos cambian estado; las consultas no
| Comandos | Consultas | |
|---|---|---|
| HTTP | POST, PUT, DELETE | GET |
| Efectos secundarios | Sí | No |
| Retorno | Entidad de dominio / void | DTO / proyección |
| Ejemplo | CreateUserCommand |
GetUsersPaginatedQuery |
Si tu handler de "crear usuario" llama a findAll() para validar algo, hay un problema de diseño — separa la lógica.
El flujo que tu equipo puede dibujar en una pizarra
POST /users → CommandBus → CreateUserHandler → UserRepositoryPort
GET /users → QueryBus → ListUsersHandler → UserRepositoryPort
Los controladores tienen cero reglas de negocio. Despachan mensajes.
Tu primer comando
export class CreateUserCommand extends Command {
constructor(
public readonly dto: { email: string; name: string; password: string },
) {
super();
}
}
El handler es dueño de las reglas
@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 ya registrado');
const password = await bcrypt.hash(command.dto.password, 10);
return this.users.create({ ...command.dto, password });
}
}
Devuelve una forma de dominio, no un DTO de Swagger. El controlador mapea la respuesta HTTP si hace falta.
Tu primera consulta
@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;
}
}
Paginación, búsqueda y ordenamiento viven en query handlers — ahí evolucionan los modelos de lectura.
Controlador delgado, equipo tranquilo
@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));
}
}
Cuándo CQRS compensa las carpetas extra
- Handlers de más de ~80 líneas o mezclan lectura/escritura.
- Añades varios puntos de entrada (HTTP + cola + CLI) reutilizando casos de uso.
- Lectura y escritura escalan distinto (reportes vs altas).
Para un CRUD de cinco endpoints, un servicio fino basta. Para una API de producto, CQRS se paga en tests y claridad.
Registra los handlers o lo pagas en runtime
Cada handler debe estar en providers del módulo. Si falta, falla en ejecución, no en compilación — añade un smoke test por módulo.
Para cerrar
CQRS no es microservicios ni Kafka el día uno. Es nombrar y separar las dos cosas que toda API hace: cambiar datos y mostrar datos. NestJS te da los buses; tu trabajo es mantener controladores tontos y handlers enfocados.
Recibe artículos por email
Sin spam — solo un aviso cuando publique algo nuevo sobre backend, cloud y arquitectura.
Un email cuando salga un artículo. Puedes darte de baja cuando quieras.
Artículos relacionados

Cómo construir una aplicación SaaS Multi-Tenant en NestJS sin duplicar tu código
Si agregar un nuevo cliente implica desplegar una nueva aplicación o copiar una base de datos completa, probablemente tu arquitectura SaaS no está preparada para escalar. Aprende cómo implementar multi-tenancy en NestJS de forma limpia y mantenible

Tu API no necesita más servicios, necesita eventos
Si cada nueva funcionalidad te obliga a modificar cinco servicios distintos, probablemente tengas un problema de acoplamiento. Aprende cómo Event-Driven Architecture ayuda a desacoplar módulos y escalar aplicaciones NestJS.

Por qué tu servicio NestJS se vuelve inmantenible (y cómo la arquitectura hexagonal lo arregla)
Controladores que saben demasiado, entidades llenas de decoradores ORM y tests que exigen base de datos. Guía práctica de puertos y adaptadores en NestJS.