Makingcode
Volver al blog

NestJS Enterprise · Parte 1 de 4

Ver todos
Arquitectura4 min de lecturaAvailable in English

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.

Por qué tu servicio NestJS se vuelve inmantenible (y cómo la arquitectura hexagonal lo arregla)

Empiezas un proyecto NestJS con buenas intenciones. Seis meses después, users.service.ts importa TypeORM, envía correos, hashea contraseñas y publica eventos en Kafka. Cambiar la base de datos parece una operación a corazón abierto.

No es un problema de equipo. Es un problema de arquitectura — y la arquitectura hexagonal (puertos y adaptadores) es una de las respuestas más efectivas cuando ya no alcanza el clásico controller → service → entity.

Este artículo sigue el modelo del NestJS Enterprise Starter, con el nivel de detalle para aplicarlo en tu propio código esta semana.

El costo real de una capa de servicios "simple"

Cuando la lógica de negocio, HTTP y persistencia viven en la misma clase:

  • Los tests necesitan Postgres — feedback lento, tests omitidos, regresiones en producción.
  • Acoplamiento al framework — los decoradores de TypeORM se convierten en tu "modelo de dominio."
  • Controladores obesos — validación, hashing y efectos secundarios terminan en el @Post().

La arquitectura hexagonal traza una frontera: el centro son tus reglas de negocio; todo lo demás es un plugin reemplazable.

La imagen que sí ayuda

Imagina tu aplicación como un hexágono:

  • Adaptadores primarios empujan trabajo hacia adentro: REST, GraphQL, CLI, consumidores de cola.
  • Adaptadores secundarios son invocados por el núcleo: PostgreSQL, Redis, Stripe, SendGrid.
  • Puertos son interfaces TypeScript que define el núcleo — "necesito guardar un usuario", no "uso TypeORM."
HTTP  →  Application (casos de uso)  →  Dominio

El repositorio TypeORM implementa UserRepositoryPort

La regla es simple: las dependencias apuntan hacia adentro. core/ nunca importa @nestjs/common ni typeorm.

Una estructura de carpetas que puedes adoptar mañana

src/
├── core/
│   ├── domain/
│   └── ports/output/persistence/
├── application/
│   └── commands/user/
├── adapters/
│   ├── primary/http/
│   └── secondary/persistence/typeorm/
└── modules/user/

Los módulos NestJS son solo cableado: enlazan UserRepositoryPortUserRepository. Los handlers dependen del puerto; el módulo elige el adaptador.

Puertos: contratos, no implementaciones

El dominio declara lo que necesita:

export abstract class UserRepositoryPort {
  abstract findByEmail(email: string): Promise<User | null>;
  abstract create(payload: CreateUserPayload): Promise<User>;
}

Sin @Injectable(), sin Repository<UserOrmEntity>. El handler pide una capacidad; NestJS inyecta la implementación en runtime.

Adaptadores: donde viven los frameworks

La entidad TypeORM queda en adapters/secondary:

@Entity('users')
export class UserOrmEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;
 
  @Column({ unique: true })
  email: string;
  // ...
}

Un mapper traduce ORM → dominio:

static toDomain(orm: UserOrmEntity): User {
  return { id: orm.id, email: orm.email, name: orm.name, ... };
}

Los handlers nunca ven UserOrmEntity. Si migras a Prisma, reescribes un adaptador — no cuarenta casos de uso.

El controlador traduce, no decide

Mal:

@Post()
async create(@Body() dto: CreateUserDto) {
  const hash = await bcrypt.hash(dto.password, 10);
  return this.repo.save({ ...dto, password: hash });
}

Bien:

@Post()
create(@Body() dto: CreateUserDto) {
  return this.commandBus.execute(new CreateUserCommand(dto));
}

Reglas de contraseña, emails duplicados y correos de bienvenida van en un command handler en la capa de aplicación.

Lo que ganas en el día a día

Beneficio Cómo se siente en la práctica
Tests rápidos Handlers con mocks jest.fn() — sin Docker
Infra intercambiable Nueva cola o BD = nuevo adaptador + provider
Onboarding Todos saben dónde va cada pieza
Reutilización El mismo handler desde HTTP, cron o worker BullMQ

Errores que repito en cada auditoría

  1. Entidades ORM en core/ — muévelas a adapters; mapea a tipos planos.
  2. Inyectar UserRepository concreto en handlers — inyecta UserRepositoryPort.
  3. "Solo esta vez" en el controlador — nunca se queda en una vez.

Siguiente paso

Este diseño encaja con CQRS (lecturas y escrituras separadas) y con puertos para colas (BullMQ detrás de IQueueService). En esta serie cubrimos esos patrones con el mismo criterio de dependencias.

Si estás construyendo una API NestJS que debe sobrevivir más de un ciclo de contratación, empieza sacando un módulo (Users) a puertos y adaptadores. La diferencia se nota en la primera semana de tests.

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