Auditoría de un proyecto NestJS con un agente de IA

Por: Artiko
nestjsbuenas-practicasauditoriaagente-iallmcode-reviewseguridad

Auditoría de un proyecto NestJS con un agente de IA

Los catorce capítulos anteriores explican qué hace de calidad a un backend NestJS y cómo se ve cada práctica en el código. Este último capítulo cierra el ciclo: convierte todo el curso en un procedimiento reproducible que un agente de IA con acceso al repositorio ejecuta para auditar un proyecto NestJS real y emitir un informe priorizado por severidad, con la evidencia (archivo:línea) que respalda cada hallazgo.

La idea no es que el agente diga “el código se ve bien”. Es que recorra diez dimensiones —las mismas del curso—, ejecute búsquedas concretas (ripgrep, grep, inspección del package.json), y para cada smell devuelva un veredicto con severidad, ubicación, impacto y una recomendación enlazada al capítulo que la explica. Un agente hace esto en minutos sobre un repo que a una persona le tomaría un día leer entero.

flowchart LR
    A[Fase 0<br/>Reconocimiento] --> B[10 dimensiones<br/>de auditoría]
    B --> C[Hallazgos con<br/>evidencia]
    C --> D[Informe<br/>priorizado]
    D -.aprobación.-> E[Remediación]

Cómo usar este capítulo

  1. Abrí una sesión limpia de tu agente (Claude Code, Cursor, Codex, etc.) en la raíz del proyecto NestJS que querés auditar.
  2. Copiá el bloque “Prompt para el agente” completo (está al final) y pegalo como primer mensaje.
  3. El agente ejecuta la Fase 0 (reconocimiento) y luego las diez dimensiones, y devuelve el informe estructurado.
  4. Regla de oro: el agente audita y reporta, no modifica nada. La remediación se propone como plan y espera tu aprobación explícita.

El capítulo primero desglosa cada fase con las búsquedas concretas para que entiendas qué hace el agente y puedas revisar su trabajo. Al final está el prompt listo para copiar.

Escala de severidad

Cada hallazgo lleva una severidad. Usá siempre esta escala, nunca un binario “está/no está”:

SeveridadSignificado
CRÍTICOVulnerabilidad explotable ya presente, o pérdida/corrupción de datos posible (p. ej. entidad con password en la respuesta, transacción ausente en una operación multi-tabla).
ALTORiesgo serio de seguridad o de rendimiento bajo carga real (N+1 en un endpoint caliente, autorización sólo por rol sin chequeo de propiedad).
MEDIODeuda de calidad que degrada mantenibilidad o rendimiento moderado (lógica de negocio en el controlador, await en serie evitable).
BAJOMejora recomendable sin impacto inmediato (console.log residual, falta de paginación en un listado chico).

Fase 0 — Reconocimiento

Antes de auditar nada, el agente construye un mapa del proyecto. Sin este contexto, muchas heurísticas dan falsos positivos (p. ej. buscar helmet en un proyecto Fastify, donde el paquete es @fastify/helmet).

Qué detectar y cómo:

# Versión de Nest y adapter (Express por defecto vs Fastify)
rg '"@nestjs/(core|common|platform-express|platform-fastify)"' package.json

# ORM / capa de datos
rg '"(@prisma/client|typeorm|@mikro-orm/core|drizzle-orm|mongoose)"' package.json

# Scripts disponibles (test, lint, e2e)
rg '"(test|test:e2e|test:cov|lint|start:prod)"' package.json

# ¿Hay validación y config centralizada?
rg '"(class-validator|class-transformer|zod|@nestjs/config|joi)"' package.json

# Estructura de módulos: cuántos módulos y cómo se organizan
fd -e module.ts
fd -e controller.ts | wc -l
fd -e service.ts | wc -l

# ¿ValidationPipe global? ¿helmet? ¿versión de Node?
rg 'useGlobalPipes|useGlobalFilters|useGlobalInterceptors' src/main.ts
rg '"node"' package.json   # engines

Con eso el agente redacta un resumen de 5-8 líneas: versión de Nest (11.x), adapter (Express/Fastify), ORM, número de módulos, si existe ConfigModule, si hay ValidationPipe global, y qué comandos de test/lint están disponibles. Ese resumen encabeza el informe y condiciona el resto: por ejemplo, si el adapter es Fastify, el agente busca @fastify/helmet y @fastify/rate-limit en vez de helmet y @nestjs/throttler sobre Express.

Si package.json declara Nest ^12 o next, el agente lo señala: cambian las recomendaciones de validación (Standard Schema), el runner de tests (Vitest en vez de Jest) y el sistema de módulos (ESM). Ver Validación y transformación.

Las diez dimensiones de auditoría

Cada dimensión tiene: qué buscar, comandos/greps concretos y el capítulo de referencia para la recomendación. El agente recorre las diez en orden.

1. Arquitectura y organización — caps. 2 y 3

Qué buscar: lógica de negocio en los controladores, acoplamiento directo al ORM desde la capa de dominio, e imports circulares entre módulos.

# Lógica de negocio en el controller: llamadas al repositorio/ORM,
# lógica condicional o cálculos dentro de métodos de *.controller.ts
rg -n 'repository|prisma|entityManager|\.save\(|\.create\(|if \(|for \(' -g '*.controller.ts'

# Controladores gordos (más de ~120 líneas suele indicar lógica mal ubicada)
fd -e controller.ts -x wc -l {} \; | sort -rn | head

# Acoplamiento al ORM en la capa de dominio/servicios de negocio
rg -n 'import .*(typeorm|@prisma/client|mongoose)' src/**/domain src/**/core 2>/dev/null

# Imports circulares: forwardRef es la señal delatora
rg -n 'forwardRef' -g '*.ts'

Señales de hallazgo: un *.controller.ts que inyecta un repositorio y arma queries; if/for con reglas de negocio dentro del handler; entidades de dominio que importan decoradores de TypeORM/Prisma. Recomendación: mover la lógica a servicios de aplicación y aislar el dominio del ORM (puertos y adaptadores). Ver Organización del proyecto y SOLID en NestJS.

2. Inyección de dependencias y providers — cap. 4

Qué buscar: Scope.REQUEST usado sin necesidad (contamina toda la cadena de dependencias y elimina el caché del contenedor), forwardRef como parche de dependencias circulares, y providers globales innecesarios.

# Scope REQUEST/TRANSIENT: cada uno reconstruye el árbol por request
rg -n 'Scope\.(REQUEST|TRANSIENT)' -g '*.ts'

# forwardRef: casi siempre delata un problema de diseño de módulos
rg -n 'forwardRef' -g '*.ts'

# Módulos globales: @Global() esparcido rompe el encapsulamiento
rg -n '@Global\(\)' -g '*.ts'

# Providers con token string mágico sin constante centralizada
rg -n "provide: ['\"]" -g '*.ts'

Señales: Scope.REQUEST en un provider que no necesita estado por request; @Global() en módulos de dominio; forwardRef entre dos servicios que podrían separarse en un tercer módulo. Impacto de Scope.REQUEST: el contenedor instancia el provider y toda su cadena en cada request, perdiendo el singleton. Ver Providers y DI avanzada.

3. Validación y transformación — cap. 5

Qué buscar: ValidationPipe sin whitelist, ValidationPipe no registrado globalmente, y validación hecha a mano dentro de servicios en vez de DTOs.

# ¿ValidationPipe global y con whitelist/forbidNonWhitelisted?
rg -n 'ValidationPipe' -g '*.ts'
rg -n 'whitelist|forbidNonWhitelisted|transform: true' src/main.ts

# DTOs sin decoradores de validación (clases *.dto.ts sin @Is*)
fd -e dto.ts -x sh -c 'rg -L "@Is|@Validate|@Type" "$1" >/dev/null || echo "sin validacion: $1"' _ {}

# Validación manual en servicios (síntoma de DTO ausente)
rg -n 'throw new BadRequestException' -g '*.service.ts'
rg -n 'typeof .* !== |=== undefined|isNaN\(' -g '*.service.ts'

Señales: app.useGlobalPipes(new ValidationPipe()) sin whitelist: true (deja pasar propiedades no declaradas, riesgo de mass assignment); DTOs que son interfaces sin decoradores; servicios llenos de if que validan tipos que un DTO resolvería. Recomendación: ValidationPipe global con whitelist: true y forbidNonWhitelisted: true. Ver Validación y transformación.

4. Ciclo de vida del request — cap. 6

Qué buscar: autorización dentro del service (debería vivir en un guard), try/catch que traga errores en vez de delegarlos a un exception filter, y stack traces expuestos al cliente.

# Autorización en el service en lugar de un Guard
rg -n 'if \(.*user\.role|req\.user|currentUser\.' -g '*.service.ts'

# try/catch que devuelve genéricos u oculta el error real
rg -n 'catch \(' -g '*.service.ts' -A2

# Stack traces / error.message crudo enviado al cliente
rg -n 'error\.stack|error\.message' -g '*.controller.ts' -g '*.filter.ts'

# ¿Existe algún exception filter global?
rg -n 'useGlobalFilters|@Catch\(' -g '*.ts'

Señales: chequeos de rol dentro de métodos de servicio (deberían ser un @UseGuards); catch que hace return null o re-lanza un Error genérico perdiendo el tipo; envío de error.stack en la respuesta. Recomendación: guards para autz, filtros para errores, y nunca serializar el stack en producción. Ver Ciclo de vida del request.

5. Rendimiento — cap. 7

Qué buscar: await en serie donde podría ir Promise.all, ausencia total de caché, y listados sin paginación.

# await consecutivos independientes (candidatos a Promise.all)
rg -n 'await ' -g '*.service.ts' -A1 | rg -B1 'await '

# ¿Se usa algo de caché?
rg -n 'CacheModule|@CacheKey|CACHE_MANAGER|cacheManager' -g '*.ts'

# Endpoints que devuelven listas sin take/limit/skip
rg -n '\.find\(\)|findAll\(\)|\.findMany\(\{\}\)' -g '*.ts'
rg -n 'take:|limit:|skip:|cursor:' -g '*.ts'   # si no aparece, no hay paginación

Señales: dos o más await seguidos sobre promesas independientes (se ejecutan en serie, sumando latencias en vez de solaparlas); ningún CacheModule; find() sin take/limit. Impacto concreto: tres await en serie de 100 ms cada uno tardan 300 ms; con Promise.all, 100 ms. Ver Rendimiento.

6. Persistencia — cap. 8

Qué buscar: N+1 queries, transacciones ausentes en operaciones multi-tabla, y SQL crudo o SELECT * sin proyección.

# N+1: find/query dentro de un map/forEach/for
rg -n 'map\(async|for .* of |forEach\(async' -g '*.service.ts' -A3 | rg 'await .*(find|query|repository)'

# Operaciones de escritura múltiple sin transacción
rg -n '\.save\(|\.create\(|\.update\(|\.delete\(' -g '*.service.ts'
rg -n 'transaction\(|\$transaction|queryRunner|manager\.transaction' -g '*.ts'  # si no aparece cerca, falta tx

# SQL crudo / QueryBuilder sin proyección
rg -n 'getManager\(|\.query\(|createQueryBuilder' -g '*.ts'
rg -n 'select \*|SELECT \*' -g '*.ts'

Señales: un await repository.find(...) dentro de un map/for (una query por iteración, O(n) round-trips); varios save/update seguidos sin un $transaction/queryRunner alrededor; .query('SELECT * ...') que trae columnas de más. Recomendación: cargar relaciones con relations/include/join, envolver escrituras en transacciones, y proyectar sólo las columnas necesarias. Ver Persistencia.

7. Seguridad — cap. 10

Qué buscar: sin helmet ni rate limiting, autorización sólo por rol (BOLA: falta el chequeo de propiedad del recurso), secretos hardcodeados, y entidades que exponen password/hash en la respuesta.

# helmet y rate limiting (según adapter)
rg -n 'helmet|@fastify/helmet' -g '*.ts'
rg -n 'ThrottlerModule|@Throttle|@fastify/rate-limit' -g '*.ts'

# BOLA: autz sólo por rol, sin comparar ownerId con el usuario actual
rg -n '@Roles\(|hasRole|user\.role ===' -g '*.ts'
rg -n 'ownerId|userId ===|\.userId ==' -g '*.service.ts'   # si no aparece, sospechar BOLA

# Secretos hardcodeados
rg -n "(secret|password|apiKey|token|jwt).{0,3}=.{0,3}['\"][A-Za-z0-9+/_-]{8,}" -g '*.ts' -i
rg -n "['\"]sk-|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}" -g '!node_modules'

# Entidad con password expuesto: falta @Exclude / select:false
rg -n 'password|passwordHash|salt' -g '*.entity.ts'
rg -n '@Exclude\(\)|select: false' -g '*.entity.ts'   # debe aparecer junto a password

Señales: ningún helmet; ningún ThrottlerModule; endpoints protegidos sólo con @Roles('user') que nunca comparan el ownerId del recurso con el req.user.id (cualquier usuario autenticado accede a recursos de otro: Broken Object Level Authorization); literales tipo jwtSecret = 'supersecret'; una entidad con password sin @Exclude() (class-transformer) ni select: false (TypeORM), que se serializa en cada respuesta. Este último es CRÍTICO. Ver Seguridad.

8. Configuración — cap. 11

Qué buscar: process.env disperso por todo el código en vez de ConfigService, y ausencia de validación del esquema de variables de entorno.

# process.env accedido fuera de la config centralizada
rg -n 'process\.env\.' -g '*.ts' -g '!*.config.ts' -g '!main.ts'

# ¿Hay ConfigModule y validación de env?
rg -n 'ConfigModule\.forRoot|ConfigService' -g '*.ts'
rg -n 'validationSchema|validate:|envSchema' -g '*.ts'   # si no aparece, no se validan las env

Señales: process.env.DATABASE_URL leído en servicios y controladores (sin tipado, sin validación, falla en runtime si falta); ConfigModule.forRoot() sin validationSchema (Joi) ni validate (Zod/class-validator). Impacto: una variable faltante o mal escrita revienta en producción en el peor momento, no al arrancar. Ver Configuración.

9. Observabilidad — cap. 12

Qué buscar: console.log en vez de un logger estructurado, y ausencia de health checks.

# console.* residual (usar Logger de Nest o pino/winston)
rg -n 'console\.(log|error|warn|debug|info)' -g '*.ts' -g '!*.spec.ts'

# ¿Hay health checks (Terminus)?
rg -n '@nestjs/terminus|HealthCheck|@Get\(.health.\)' -g '*.ts'

# ¿Logger estructurado?
rg -n 'nestjs-pino|winston|new Logger\(' -g '*.ts'

Señales: console.log en código de producción (sin niveles, sin correlación, sin formato JSON parseable por el agregador); ningún endpoint /health ni @nestjs/terminus. Recomendación: Logger de Nest o nestjs-pino, y un health check con Terminus que verifique DB y servicios upstream. Ver Observabilidad.

10. Testing — cap. 13

Qué buscar: mocks excesivos que acoplan el test a la implementación, ausencia de tests e2e, y falta de cobertura de los caminos de error.

# Densidad de mocks: muchos jest.fn()/mock por spec indica sobre-mockeo
rg -c 'jest\.fn\(\)|mockResolvedValue|createMock' -g '*.spec.ts' | sort -t: -k2 -rn | head

# ¿Existen tests e2e?
fd -e e2e-spec.ts
rg -n 'supertest|request\(app' -g '*.ts'

# ¿Se testean los caminos de error? (expect a que se lance)
rg -n 'toThrow|rejects\.toThrow|expect.*Exception' -g '*.spec.ts'   # escasez = sólo happy path

Señales: specs con decenas de jest.fn() que mockean hasta las dependencias internas (el test verifica la implementación, no el comportamiento; se rompe con cualquier refactor); cero archivos *.e2e-spec.ts; ausencia de toThrow/rejects.toThrow (sólo se prueba el camino feliz). Ver Testing.

Tabla resumen de heurísticas

Para revisar el trabajo del agente de un vistazo, este es el mapa smell → dónde mirar → señal:

DimensiónBúsqueda claveSeñal de hallazgoCap.
Arquitecturarepository|prisma en *.controller.tsController que arma queries2, 3
DIScope.REQUEST, forwardRefScope abusado, ciclo parcheado4
Validaciónwhitelist en main.tsValidationPipe sin whitelist5
Requestif.*user.role en *.service.tsAutz en el service6
Rendimientoawait seguidos, CacheModuleSerie evitable, sin caché7
Persistenciafind dentro de map/forN+1 queries8
Persistenciasave/update sin $transactionTransacción ausente8
Seguridadpassword sin @Exclude en *.entity.tsPassword serializado (CRÍTICO)10
Seguridad@Roles sin chequeo de ownerIdBOLA10
Seguridadhelmet, ThrottlerModule ausentesSin hardening HTTP10
Configuraciónprocess.env fuera de *.config.tsEnv dispersa, sin validar11
Observabilidadconsole.log, terminus ausenteLog no estructurado, sin health12
TestingtoThrow escaso, sin *.e2e-spec.tsSólo happy path, sin e2e13

Cuidado con los falsos positivos. Un console.log en un script de seed no es un hallazgo; Scope.REQUEST en un provider de contexto multi-tenant puede ser correcto. El agente debe leer el contexto de cada coincidencia antes de reportarla, no listar greps en crudo.

Formato del informe de auditoría

Cada hallazgo se reporta con esta estructura fija:

- **[SEVERIDAD] Título del hallazgo** — dimensión
  - Ubicación: `ruta/archivo.ts:línea`
  - Impacto: qué consecuencia técnica tiene (rendimiento, seguridad, mantenibilidad)
  - Recomendación: qué cambio aplicar + [enlace al capítulo](/tecnologias/nestjs-buenas-practicas/NN-slug/)

Y el informe completo sigue esta plantilla:

# Auditoría NestJS — <nombre del proyecto>

## Resumen del stack (Fase 0)
- Nest: 11.x · Adapter: <Express|Fastify> · ORM: <Prisma|TypeORM|…>
- Módulos: N · ConfigModule: <sí|no> · ValidationPipe global: <sí|no>
- Comandos: test=<…>, lint=<…>, e2e=<…>

## Tabla de hallazgos
| # | Dimensión | Hallazgo | Severidad | Ubicación |
|---|-----------|----------|-----------|-----------|
| 1 | Seguridad | Entidad User serializa password | CRÍTICO | user.entity.ts:14 |
| 2 | Persistencia | N+1 al listar pedidos | ALTO | orders.service.ts:42 |
| 3 | Configuración | DATABASE_URL vía process.env | MEDIO | app.service.ts:8 |
| … | … | … | … | … |

## Score
- CRÍTICO: N · ALTO: N · MEDIO: N · BAJO: N
- Dimensiones sin hallazgos: <lista>

## Los 3 hallazgos más urgentes (detalle)
- **[CRÍTICO] La entidad User expone el hash de la contraseña** — Seguridad
  - Ubicación: `src/users/user.entity.ts:14`
  - Impacto: el campo `password` se serializa en toda respuesta que devuelva un User; se filtra en `GET /users/:id`.
  - Recomendación: agregar `@Exclude()` (class-transformer) o `select: false` (TypeORM) y activar `ClassSerializerInterceptor`. Ver [Seguridad](/tecnologias/nestjs-buenas-practicas/10-seguridad/).
- **[ALTO] N+1 al listar pedidos con sus ítems** — Persistencia
  - Ubicación: `src/orders/orders.service.ts:42`
  - Impacto: un `find` de items por cada pedido dentro de un `map`; 100 pedidos = 101 queries.
  - Recomendación: cargar con `relations: ['items']` / `include`. Ver [Persistencia](/tecnologias/nestjs-buenas-practicas/08-persistencia/).
- **[ALTO] Endpoint sin chequeo de propiedad (BOLA)** — Seguridad
  - Ubicación: `src/orders/orders.controller.ts:31`
  - Impacto: `@Roles('user')` permite a cualquier usuario leer pedidos ajenos vía `GET /orders/:id`.
  - Recomendación: guard que compare `order.userId` con `req.user.id`. Ver [Seguridad](/tecnologias/nestjs-buenas-practicas/10-seguridad/).

## Plan de remediación priorizado
### CRÍTICO
- [ ] #1 Ocultar `password` de la serialización — user.entity.ts
### ALTO
- [ ] #2 Resolver N+1 en listado de pedidos — orders.service.ts
- [ ] #3 Agregar chequeo de propiedad (BOLA) — orders.controller.ts
### MEDIO / BAJO
- [ ] #… 

> Espero tu aprobación antes de modificar ningún archivo.

Prompt para el agente

Copiá todo el bloque siguiente y pegalo como primer mensaje a tu agente, con la sesión abierta en la raíz del proyecto NestJS.

# MISIÓN: Auditoría de calidad de un proyecto NestJS

Sos un auditor de backends NestJS. Tu misión es evaluar ESTE proyecto contra las
buenas prácticas de arquitectura, rendimiento, persistencia, seguridad,
configuración, observabilidad y testing, y emitir un informe priorizado por
severidad con evidencia en `archivo:línea`.

REGLA DE ORO: NO modifiques ningún archivo. Solo leés, buscás y reportás. Toda
remediación se propone como plan y espera mi aprobación explícita.

## FASE 0 — Reconocimiento
Inspeccioná `package.json`, `src/main.ts` y la estructura de carpetas para
determinar: versión de NestJS, adapter (Express/Fastify), ORM, número de módulos,
si hay ConfigModule, si el ValidationPipe es global, y qué comandos de test/lint
existen. Resumí en 5-8 líneas. Este resumen condiciona el resto (p. ej. en
Fastify buscá `@fastify/helmet`, no `helmet`).

## FASE 1 — Auditoría por dimensión
Recorré estas 10 dimensiones. Para cada smell usá ripgrep/grep, LEÉ el contexto de
cada coincidencia (evitá falsos positivos) y reportá sólo hallazgos reales:

1. Arquitectura: lógica de negocio en controllers, acoplamiento al ORM en el
   dominio, imports circulares (`forwardRef`).
2. DI: `Scope.REQUEST`/`TRANSIENT` abusado, `forwardRef`, `@Global()` innecesario.
3. Validación: `ValidationPipe` no global o sin `whitelist`, DTOs sin decoradores,
   validación manual en servicios.
4. Ciclo de request: autz dentro del service, `try/catch` que traga errores,
   `error.stack`/`error.message` expuesto al cliente, sin exception filter.
5. Rendimiento: `await` en serie evitable (usar `Promise.all`), sin caché, listados
   sin paginación (`take`/`limit`).
6. Persistencia: N+1 (`find`/`query` dentro de `map`/`for`), escrituras múltiples
   sin transacción, SQL crudo / `SELECT *`.
7. Seguridad: sin `helmet` ni rate limiting, BOLA (autz sólo por rol sin chequear
   propiedad del recurso), secretos hardcodeados, entidad con `password`/hash sin
   `@Exclude()`/`select:false` (esto es CRÍTICO).
8. Configuración: `process.env` disperso fuera de la config central, sin validación
   de variables de entorno.
9. Observabilidad: `console.log` en producción, sin health checks (Terminus).
10. Testing: mocks excesivos acoplados a la implementación, sin tests e2e, sin
    cobertura de caminos de error (`toThrow`).

Severidad: CRÍTICO (explotable / pérdida de datos) · ALTO (riesgo serio) ·
MEDIO (deuda de calidad) · BAJO (mejora recomendable).

## FASE 2 — Informe
Entregá, en este orden:
1. Resumen del stack (Fase 0).
2. Tabla de hallazgos: # | dimensión | hallazgo | severidad | archivo:línea.
3. Score por severidad y dimensiones sin hallazgos.
4. Los 3 hallazgos más urgentes en detalle (ubicación, impacto, recomendación).
5. Plan de remediación priorizado con checkboxes, agrupado por severidad.

Cada hallazgo cita su evidencia (`archivo:línea` y el fragmento). No inventes
ubicaciones. Esperá mi aprobación antes de tocar ningún archivo.

Automatizar parte de la auditoría en CI

El agente da profundidad y contexto, pero varias dimensiones se prestan a gates automáticos en cada Pull Request, sin esperar una auditoría manual:

La combinación ideal: CI atrapa las regresiones conocidas en cada commit, y la auditoría con agente descubre lo que ningún linter tiene regla —lógica de negocio filtrada al controlador, BOLA, transacciones ausentes, sobre-mockeo—.

Checklist


Con esto cerrás el curso. Recorriste el camino completo: de los cimientos de arquitectura e IoC, a la mecánica de providers, validación y ciclo de request; del rendimiento y la persistencia, a la seguridad, la configuración, la observabilidad y el testing; y terminaste con un catálogo de patrones y este protocolo que convierte todo ese conocimiento en un procedimiento que un agente ejecuta sobre tu propio código. Ahora tenés el criterio de ingeniería para que un backend NestJS aguante producción real, y una herramienta para verificarlo de forma reproducible.

¡Felicitaciones por completar NestJS: Buenas Prácticas! Volvé cuando quieras al índice del curso para repasar cualquier capítulo.