Librerías del ecosistema y catálogo de patrones/antipatrones

Por: Artiko
nestjsbuenas-practicaspatronesantipatronesecosistemacode-reviewarquitectura

Librerías del ecosistema y catálogo de patrones/antipatrones

Este capítulo es el que vas a tener abierto cuando revises código. Los trece capítulos anteriores explicaron el por qué de cada decisión; acá consolidamos todo en dos herramientas de trabajo diario:

  1. Un mapa del ecosistema: qué librería resuelve cada necesidad, con criterio explícito de cuándo elegir una u otra. No hay “la mejor librería” en abstracto; hay trade-offs entre mantenimiento, type-safety, peso y acoplamiento.
  2. Un catálogo de patrones y antipatrones con formato de checklist. Cada patrón dice qué hacer; cada antipatrón dice cómo se detecta (síntoma), por qué genera deuda (por qué duele) y hacia dónde refactorizar, enlazando al capítulo que lo trata en profundidad.

La meta es que puedas mirar un pull request y, en minutos, decir “esto es un god-service, andá al capítulo 3” o “acá falta whitelist, capítulo 5”, con criterio técnico y no con opinión.

Parte 1 — El ecosistema recomendado

NestJS es deliberadamente agnóstico: te da el contenedor IoC, el sistema de módulos y las capas del request, pero deja las decisiones de librería en tus manos. Elegir bien importa porque cada dependencia que entra al AppModule es acoplamiento que vas a arrastrar por años.

Criterios de evaluación

Antes de la tabla, fijá los cuatro ejes con los que se compara cualquier candidata:

flowchart LR
  N[Tu necesidad] --> M{Evaluá}
  M --> A[Mantenimiento<br/>releases, seguridad]
  M --> B[Type-safety<br/>inferencia desde esquema]
  M --> C[Peso<br/>arranque, superficie]
  M --> D[Acoplamiento<br/>detrás de un puerto?]
  A & B & C & D --> R[Decisión con trade-offs<br/>explícitos]

Validación de entrada

LibreríaCuándo elegirlaTrade-off principal
class-validator + class-transformerEs el camino por defecto de Nest 11: DTOs con decoradores y ValidationPipe global. Máxima integración con Swagger y el ecosistema.La verdad vive en decoradores, no en el sistema de tipos: el tipo de la clase y las reglas pueden divergir. Depende de reflect-metadata.
ZodCuando querés que el esquema sea la única fuente de verdad y derivar el tipo con z.infer. Ideal si ya usás Zod en el resto del stack o compartís esquemas con el frontend.Requiere un pipe puente (ZodValidationPipe) o nestjs-zod; la integración con Swagger no es tan directa como con decoradores.
ValibotMismo modelo que Zod pero modular y de menor peso: importás solo los validadores que usás, con mejor tree-shaking. Útil en funciones serverless donde el arranque en frío pesa.Ecosistema más chico que Zod; menos ejemplos y menos integraciones listas para Nest.

Dirección de Nest 12: adopta Standard Schema, la interfaz común que implementan Zod, Valibot y ArkType. Eso significa que el ValidationPipe podrá aceptar cualquier esquema Standard Schema sin adaptador propietario, y la elección entre Zod y Valibot pasa a ser puramente de peso y preferencia. Ver capítulo 5 para el detalle de pipes y whitelisting: Validación y transformación.

ORM y acceso a datos

LibreríaCuándo elegirlaTrade-off principal
PrismaCuando querés máxima type-safety y una DX excelente: el esquema declarativo genera el cliente tipado y las migraciones. Mejor opción por defecto para equipos que valoran seguridad de tipos por encima de control fino del SQL.Cliente generado (paso de build extra); el control del SQL crudo es menor y las queries muy dinámicas se vuelven verbosas.
DrizzleCuando querés SQL con tipos, sin capa de generación: escribís queries cercanas a SQL con inferencia completa. Ideal para quien piensa en SQL y quiere control fino con arranque liviano (serverless/edge).Menos “batteries included”: más código para relaciones y patrones que Prisma resuelve solo. Ecosistema más joven.
TypeORMCuando el proyecto ya lo usa o necesitás el patrón Active Record / Data Mapper clásico con decoradores de entidad muy integrados a Nest.Historial de inconsistencias de tipos y comportamiento en relaciones lazy; el patrón de decoradores acopla tus entidades al ORM.

El eje decisivo acá es acoplamiento: cualquiera de los tres debe vivir detrás de un puerto de repositorio (capítulo 8) para que el dominio no dependa de la librería. El N+1, las transacciones y el Unit of Work se tratan en Persistencia.

Autenticación y autorización

LibreríaRolCuándo
@nestjs/passport + PassportEstrategias de autenticación (local, JWT, OAuth2, OIDC)Cuando necesitás múltiples proveedores de identidad o el flujo estándar de estrategias. Es el estándar de facto.
@nestjs/jwtFirma y verificación de tokens JWTCuando emitís tus propios tokens. Se combina con Passport (JwtStrategy) o se usa directo en un guard.
CASLAutorización basada en habilidades (ABAC), no solo rolesCuando la autorización depende de atributos del recurso (ownership, estado, tenant) y no de un rol plano. Previene el BOLA (autorización a nivel de objeto rota).

La regla de oro: la autenticación te dice quién sos; la autorización te dice qué podés hacer con este recurso. Resolver autorización solo por rol es el antipatrón que abre el BOLA. Todo el detalle está en Seguridad.

Colas, eventos y CQRS

LibreríaCuándo elegirlaTrade-off
BullMQ (@nestjs/bullmq)Trabajos en background durables: emails, procesamiento de archivos, reintentos con backoff, jobs programados. Persiste en Redis, sobrevive reinicios.Suma Redis como dependencia de infraestructura. Es la opción correcta cuando el trabajo no puede perderse.
@nestjs/event-emitterEventos de dominio in-process, desacople dentro del mismo proceso (ej. “usuario registrado” dispara side-effects).Los eventos viven en memoria: si el proceso cae, se pierden. No lo uses para trabajo que deba completarse sí o sí.
@nestjs/cqrsCuando el dominio se beneficia de separar comandos (escritura) de queries (lectura) y modelar eventos de dominio explícitos con handlers.Añade indirección y ceremonia; injustificado en CRUD simple. Vale cuando la lógica de escritura es rica.

El criterio: durabilidad manda. Si perder el trabajo es aceptable, EventEmitter alcanza; si no, BullMQ. CQRS es ortogonal y se justifica por complejidad del dominio, no por moda. Ver Async, colas y eventos.

Configuración, logs y observabilidad

LibreríaRolNota de criterio
@nestjs/configCarga y valida variables de entornoValidá el esquema al arranque (fail-fast) y exponé config tipada por namespace. Nunca leas process.env disperso.
nestjs-pinoLogging estructurado en JSON de alto rendimientoPino serializa a JSON con overhead mínimo frente a loggers que formatean strings. Correlacioná con un requestId.
OpenTelemetryTrazas distribuidas y métricasEstándar vendor-neutral: instrumentás una vez y exportás a Jaeger, Tempo, Datadog, etc. Evita atarte a un APM propietario.
@nestjs/terminusHealth checks (/health, readiness/liveness)Necesario para orquestadores (Kubernetes) que deciden reinicios y tráfico según el estado real de dependencias.

El detalle de arranque validado está en Configuración y el de trazas/logs/health en Observabilidad.

Documentación de API

LibreríaRolNota
@nestjs/swaggerGenera OpenAPI desde tus DTOs y decoradoresSi ya validás con class-validator, buena parte del esquema OpenAPI sale gratis. Documentá códigos de error y ejemplos, no solo el happy path. Mantené el contrato como fuente para clientes generados.

Resumen: cómo decidir

flowchart TD
  Q[Necesito una librería para X] --> V{Está mantenida<br/>y alineada a Nest 11?}
  V -- No --> STOP[Descartar:<br/>deuda futura]
  V -- Sí --> T{Deriva tipos<br/>desde el esquema?}
  T -- Sí --> W{Peso aceptable<br/>para mi runtime?}
  T -- No --> W2[Evaluar si el acoplamiento<br/>a decoradores es tolerable]
  W -- Sí --> P{Puedo ponerla<br/>detrás de un puerto?}
  W -- No --> LIGHT[Buscar alternativa<br/>modular / tree-shakeable]
  P -- Sí --> OK[Adoptar]
  P -- No --> RISK[Adoptar solo si es<br/>infraestructura de borde]

Parte 2 — Catálogo de patrones

Estos son los patrones que el curso recomienda de forma transversal. Cada uno resuelve una tensión concreta de SOLID, rendimiento o seguridad.

PatrónEn qué consistePor qué valeCapítulo
Controllers finosEl controller solo traduce HTTP ↔ caso de uso: valida el DTO, invoca un servicio, devuelve la respuesta. Cero lógica de negocio.Mantiene testeable el dominio sin levantar HTTP y respeta SRP.03
Servicios con SRPUn servicio, una responsabilidad cohesiva. Si un servicio hace auth, mailing y facturación, se parte.Reduce el radio de impacto de cada cambio y facilita el reemplazo de partes.03
Puertos + adaptadores con tokens (DIP)El dominio depende de una interfaz (PAGOS_PORT), no de la implementación. Se inyecta con un token vía useClass/useFactory.Invierte la dependencia: el dominio no conoce Stripe ni Prisma. Testeable con dobles.02, 04
DTOs validados con whitelistValidationPipe global con whitelist: true y forbidNonWhitelisted: true; DTOs explícitos por endpoint.Descarta propiedades no declaradas: previene mass assignment y contamina menos el dominio.05
Exception filters globalesUn filtro central mapea errores de dominio a respuestas HTTP consistentes (código, forma, sin filtrar stack).Respuestas uniformes y sin fugas de información; el dominio lanza errores semánticos, no HttpException.06
Interceptores para cross-cuttingLogging, timing, serialización, cache y transformación de respuesta viven en interceptores, no en cada handler.Elimina duplicación y saca lo transversal del código de negocio (OCP).06
Guards para autorización con ownershipGuards que verifican no solo el rol sino que el recurso pertenezca (o sea accesible) al sujeto.Cierra el BOLA: la autorización considera el objeto, no solo la identidad.10
Config validada al arranqueEsquema de env vars validado en el bootstrap; si falta o es inválida, el proceso no levanta.Fail-fast: los errores de configuración se ven al desplegar, no en el primer request en producción.11
Repositorios abstraídosEl acceso a datos vive detrás de una interfaz de repositorio; el ORM es un detalle del adaptador.Aísla el dominio del ORM, habilita cambiar de Prisma a Drizzle y testear sin base real.08

Ejemplo condensado del patrón puerto + token, que es el corazón de todo lo demás:

// Puerto (dominio, sin dependencias del framework de datos)
export interface PagosPort {
  cobrar(cuenta: string, monto: number): Promise<Resultado>;
}
export const PAGOS_PORT = Symbol('PAGOS_PORT');

// Adaptador (infraestructura)
@Injectable()
export class StripePagosAdapter implements PagosPort {
  async cobrar(cuenta: string, monto: number): Promise<Resultado> {
    /* llamada a Stripe */
  }
}

// Módulo: el dominio pide PagosPort, no StripePagosAdapter
@Module({
  providers: [{ provide: PAGOS_PORT, useClass: StripePagosAdapter }],
  exports: [PAGOS_PORT],
})
export class PagosModule {}

// Servicio de dominio: no sabe que existe Stripe
@Injectable()
export class CheckoutService {
  constructor(@Inject(PAGOS_PORT) private readonly pagos: PagosPort) {}
}

Parte 3 — Catálogo de antipatrones

Formato síntoma → por qué duele → refactor. Usalo como grilla de code review: si ves el síntoma, sabés a qué capítulo mandar.

AntipatrónSíntoma (cómo se detecta)Por qué dueleRefactorCapítulo
Lógica de negocio en el controllerEl controller arma queries, calcula precios o valida reglas de dominio; tiene más de 5-10 líneas por handler.No se puede testear sin levantar HTTP; la regla se duplica en cada endpoint que la necesita.Mover la regla a un servicio/caso de uso; el controller solo orquesta.03
God-serviceUn *.service.ts de cientos de líneas con 15+ métodos de dominios distintos y media docena de dependencias inyectadas.Cualquier cambio lo toca; imposible razonar sobre él; los tests son enormes y frágiles.Partir por responsabilidad cohesiva; extraer servicios y puertos.03
N+1 queriesUn for/map que hace await repo.find(...) por cada elemento; los logs muestran cientos de queries casi idénticas.Latencia lineal con los datos: 1 lista + N detalles. Un endpoint que anda con 10 filas colapsa con 10.000.Cargar en lote (in), usar include/join o un DataLoader.08
REQUEST scope abusadoProviders marcados Scope.REQUEST sin necesidad real, propagándose por toda la cadena de DI.Nest instancia el subárbol por cada request: más GC, peor throughput. El scope “sube” e infecta a los dependientes.Volver a scope singleton por defecto; pasar datos de request como argumento, no por DI.04
forwardRef como parcheAparecen forwardRef(() => X) para romper ciclos de dependencia entre módulos/servicios.Oculta un problema de diseño: dos módulos mutuamente dependientes. Frágil ante refactors y difícil de seguir.Extraer la abstracción compartida a un tercer módulo; invertir la dependencia con un puerto.04
Autorización solo por rol (BOLA)El guard chequea role === 'user' pero nunca que el recurso pertenezca al usuario; GET /orders/:id devuelve la orden de cualquiera.Broken Object Level Authorization: el #1 del OWASP API Top 10. Fuga de datos entre usuarios/tenants.Verificar ownership del objeto (guard o política CASL) además del rol.10
Secretos hardcodeadosClaves de API, tokens o passwords literales en el código o commiteados en .env versionado.Fuga en cuanto se filtra el repo; rotación imposible sin redeploy; auditoría rota.Cargar desde entorno/secret manager vía @nestjs/config; validar presencia al arranque.11
process.env dispersoprocess.env.X leído directo en servicios, a mano, sin validación ni tipos, repartido por todo el código.Un typo en la key devuelve undefined silencioso; imposible saber qué config usa el sistema; sin fail-fast.Centralizar en ConfigModule con esquema validado y config tipada por namespace.11
Mockear todo en testsTests unitarios con 8 mocks que en la práctica prueban que los mocks se llaman entre sí.No detectan bugs reales de integración; se rompen ante cualquier refactor interno aunque el comportamiento no cambie.Testear comportamiento con dobles solo en los bordes (I/O); integración con Testcontainers para lo demás.13
console.log en producciónconsole.log/console.error sembrados en handlers y servicios.Sin estructura ni niveles ni correlación; bloqueante y sin formato para agregadores; ruido imposible de filtrar.Logger estructurado (nestjs-pino) con niveles y requestId.12
Sin rate limitingNo hay @nestjs/throttler ni límite en endpoints sensibles (login, reset de password, búsqueda).Fuerza bruta, scraping y DoS trivial; abuso de recursos costosos.Aplicar throttling global y límites más estrictos en endpoints sensibles.10
DTOs sin whitelistValidationPipe sin whitelist, o directo sin DTO: el handler recibe el body crudo.Mass assignment: el cliente manda isAdmin: true y se persiste. Propiedades no previstas contaminan el dominio.ValidationPipe global con whitelist: true y forbidNonWhitelisted: true; DTO explícito por endpoint.05

Los dos antipatrones más caros ilustrados en código:

// ANTIPATRÓN — lógica de negocio en el controller + N+1
@Controller('orders')
export class OrdersController {
  constructor(private readonly repo: OrderRepo) {}

  @Get()
  async list(@Query('userId') userId: string) {
    const orders = await this.repo.findByUser(userId);
    // N+1: una query de items por cada orden
    for (const o of orders) {
      o.items = await this.repo.findItems(o.id);
      // regla de negocio incrustada en el controller
      o.total = o.items.reduce((s, i) => s + i.price * i.qty, 0);
    }
    return orders;
  }
}
// MEJOR — controller fino, carga en lote, regla en el dominio
@Controller('orders')
export class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}

  @Get()
  list(@Query() query: ListOrdersDto) {
    return this.ordersService.listWithTotals(query.userId);
  }
}

@Injectable()
export class OrdersService {
  constructor(@Inject(ORDER_REPO) private readonly repo: OrderRepoPort) {}

  async listWithTotals(userId: string): Promise<OrderView[]> {
    // una sola query con los items incluidos: sin N+1
    const orders = await this.repo.findByUserWithItems(userId);
    return orders.map((o) => ({ ...o, total: calcularTotal(o.items) }));
  }
}

Parte 4 — Señales de alarma (code smells)

Estos no son antipatrones cerrados: son indicios de deuda que ameritan mirar más de cerca. Si aparecen varios juntos, casi seguro hay uno de los antipatrones de arriba escondido.

El catálogo, de un vistazo

mindmap
  root((Catálogo<br/>NestJS))
    Patrones
      Controllers finos
      Servicios con SRP
      Puertos + tokens DIP
      DTOs con whitelist
      Exception filters globales
      Interceptores cross-cutting
      Guards con ownership
      Config validada al arranque
      Repositorios abstraídos
    Antipatrones
      Lógica en el controller
      God-service
      N+1 queries
      REQUEST scope abusado
      forwardRef como parche
      BOLA autz solo por rol
      Secretos hardcodeados
      process.env disperso
      Mockear todo
      console.log en prod
      Sin rate limiting
      DTOs sin whitelist
    Señales de alarma
      Servicio gigante
      any en los bordes
      HTTP en el dominio
      await en for
      Tests frágiles

Checklist

Usá esta lista al revisar cualquier módulo o pull request de NestJS:

Diseño y responsabilidades

Datos y rendimiento

Validación y seguridad

Configuración y observabilidad

Testing

Ecosistema

Con este catálogo tenés el “qué” del código de calidad en NestJS. El último paso es volverlo ejecutable: convertir cada ítem de esta checklist en una heurística que un agente de IA pueda aplicar automáticamente sobre tu repositorio, con comandos, detección y formato de informe. Eso es exactamente lo que arma el capítulo final: Auditoría con agente IA →