Makingcode
Volver al blog

NestJS Enterprise · Parte 2 de 4

Ver todos
Arquitectura3 min de lecturaAvailable in English

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.

CQRS en NestJS: deja de mezclar lecturas y escrituras en el mismo servicio

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 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