Librerías del ecosistema y catálogo de patrones/antipatrones
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:
- 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.
- 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:
- Mantenimiento: frecuencia de releases, respuesta a issues de seguridad, alineación con la versión actual de Nest, tamaño de la comunidad. Una librería sin commits en 18 meses es deuda futura.
- Type-safety: cuánto del contrato vive en el sistema de tipos de TypeScript. Una librería que infiere tipos desde el esquema (Zod, Prisma, Drizzle) elimina una clase entera de bugs que otra que usa tipos “a mano” no.
- Peso: tamaño instalado y superficie de API. En backend el bundle importa menos que en frontend, pero el peso se traduce en tiempo de arranque en frío (serverless), superficie de vulnerabilidades y curva de aprendizaje.
- Acoplamiento: cuánto del framework te obliga a mezclar en tu dominio. Una librería que se inyecta detrás de un puerto (capítulo 2) es reemplazable; una que decora tus entidades de negocio con sus decoradores te ata.
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ía | Cuándo elegirla | Trade-off principal |
|---|---|---|
| class-validator + class-transformer | Es 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. |
| Zod | Cuando 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. |
| Valibot | Mismo 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
ValidationPipepodrá 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ía | Cuándo elegirla | Trade-off principal |
|---|---|---|
| Prisma | Cuando 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. |
| Drizzle | Cuando 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. |
| TypeORM | Cuando 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ía | Rol | Cuándo |
|---|---|---|
| @nestjs/passport + Passport | Estrategias 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/jwt | Firma y verificación de tokens JWT | Cuando emitís tus propios tokens. Se combina con Passport (JwtStrategy) o se usa directo en un guard. |
| CASL | Autorización basada en habilidades (ABAC), no solo roles | Cuando 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ía | Cuándo elegirla | Trade-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-emitter | Eventos 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/cqrs | Cuando 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ía | Rol | Nota de criterio |
|---|---|---|
| @nestjs/config | Carga y valida variables de entorno | Validá el esquema al arranque (fail-fast) y exponé config tipada por namespace. Nunca leas process.env disperso. |
| nestjs-pino | Logging estructurado en JSON de alto rendimiento | Pino serializa a JSON con overhead mínimo frente a loggers que formatean strings. Correlacioná con un requestId. |
| OpenTelemetry | Trazas distribuidas y métricas | Estándar vendor-neutral: instrumentás una vez y exportás a Jaeger, Tempo, Datadog, etc. Evita atarte a un APM propietario. |
| @nestjs/terminus | Health 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ía | Rol | Nota |
|---|---|---|
| @nestjs/swagger | Genera OpenAPI desde tus DTOs y decoradores | Si 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ón | En qué consiste | Por qué vale | Capítulo |
|---|---|---|---|
| Controllers finos | El 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 SRP | Un 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 whitelist | ValidationPipe 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 globales | Un 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-cutting | Logging, 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 ownership | Guards 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 arranque | Esquema 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ídos | El 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ón | Síntoma (cómo se detecta) | Por qué duele | Refactor | Capítulo |
|---|---|---|---|---|
| Lógica de negocio en el controller | El 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-service | Un *.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 queries | Un 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 abusado | Providers 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 parche | Aparecen 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 hardcodeados | Claves 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 disperso | process.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 tests | Tests 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ón | console.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 limiting | No 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 whitelist | ValidationPipe 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.
- Un archivo
*.service.tsde más de ~300 líneas o con más de 5 dependencias inyectadas → candidato a god-service. anyoas unknown asen los bordes de datos → validación o tipado que se rindió; bugs esperando.- Métodos de servicio que reciben
Request/Response→ el dominio conoce HTTP; acoplamiento a la capa web. importsde un módulo que crecen sin parar → el módulo perdió su cohesión de dominio.try/catchque traga el error y devuelvenull→ errores invisibles; el exception filter nunca se entera.- Tests que se rompen ante cualquier refactor sin cambio de comportamiento → tests acoplados a la implementación (exceso de mocks).
- Números y strings mágicos repetidos (roles, estados, límites) → deberían ser config validada o enums.
awaitdentro de unforsobre una colección → sospechá N+1 o serialización innecesaria (podría serPromise.allo carga en lote).forwardRefen más de un lugar → problema de límites entre módulos, no un caso aislado.- Endpoints sin decorador de autorización en un controller que sí tiene otros protegidos → probable hueco de autz.
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
- Los controllers solo traducen HTTP ↔ caso de uso; no hay lógica de negocio en ellos.
- Ningún servicio concentra múltiples dominios (sin god-services).
- Las dependencias externas (ORM, pagos, mail) viven detrás de un puerto con token, no inyectadas directas.
- No hay
forwardRefcomo parche de ciclos; los límites de módulo son claros.
Datos y rendimiento
- No hay
awaitdentro de loops que generen N+1; las cargas son en lote o coninclude/join. - Los providers
Scope.REQUESTestán justificados; el resto es singleton. - Los repositorios están abstraídos detrás de una interfaz.
Validación y seguridad
-
ValidationPipeglobal conwhitelist: trueyforbidNonWhitelisted: true. - Hay DTO explícito por endpoint; nada de body crudo.
- La autorización verifica ownership del recurso, no solo el rol (sin BOLA).
- Rate limiting aplicado, con límites estrictos en endpoints sensibles.
- Cero secretos hardcodeados; todo viene de config validada.
Configuración y observabilidad
- La config se lee desde
@nestjs/configcon esquema validado al arranque (fail-fast); no hayprocess.envdisperso. - Logging estructurado con niveles y
requestId; sinconsole.logen producción. - Hay health checks y trazas/métricas donde importan.
Testing
- Los tests prueban comportamiento, no la interacción entre mocks.
- La integración usa dobles solo en los bordes de I/O.
Ecosistema
- Cada librería nueva pasó el filtro: mantenida, con type-safety razonable, peso aceptable y posible de aislar detrás de un puerto.
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 →