Nest

Domina la descripción del puesto

Guía de Estudio

Cada requisito que un rol senior de NestJS / Node.js comúnmente pide — explicado a profundidad senior. Lee el concepto, luego la línea de “cómo decirlo” para que puedas dar una respuesta precisa y concisa en voz alta.

Cómo usar esto Cada tema explica el concepto a la profundidad que espera una entrevista senior de NestJS / Node.js, y luego ofrece una línea de "cómo decirlo" — la oración precisa para decir en voz alta. Lee para comprender primero; ensaya las frases al final. Todo está actualizado a NestJS 11 y Node.js 24 LTS.

01 · NestJS, TypeScript y el modelo del framework

Lo que quieren: entiendas por qué existe Nest. Es un framework progresivo y opiniado de Node.js escrito en TypeScript que se ejecuta sobre un adaptador HTTP (Express por defecto o Fastify) y añade un contenedor IoC/DI, un sistema de módulos y decoradores. Resuelve la brecha de arquitectura que Express deja abierta.

Nest combina OOP, FP y FRP (RxJS en interceptores/microservicios). Se apoya en los metadatos de decoradores emitidos por TypeScript (reflect-metadata) para conocer el tipo de un parámetro del constructor — que es el token de inyección. Por eso tsconfig necesita experimentalDecorators + emitDecoratorMetadata, y por qué DI funciona en clases, no en interfaces.

Cómo decirlo "Nest es estructura e inyección de dependencias sobre la capa HTTP de Node — Express con una arquitectura opiniada, escrito en TypeScript y modelado en el sistema módulo/proveedor de Angular."

02 · Módulos y encapsulamiento

Core: un módulo (@Module) agrupa controladores y proveedores relacionados; la aplicación es un árbol de módulos con raíz en AppModule. Los proveedores son privados de su módulo a menos que se exporten; exports es la API pública del módulo y imports trae las exportaciones de otro módulo.

  • Cada módulo es un singleton — exporta un proveedor una vez y todos los importadores comparten una instancia.
  • Listar la misma clase en los providers de dos módulos crea dos instancias (un bug de estado compartido).
  • @Global() expone exportaciones en todas partes sin importar — reserve para infraestructura transversal (config, DB, logging); su uso excesivo oculta acoplamiento.
Cómo decirlo "Los módulos te dan encapsulamiento: un proveedor es privado hasta que lo exporto. Mantengo los límites explícitos y evito @Global() excepto para infraestructura verdadera."

03 · Controladores y routing

Core: los controladores (@Controller('users')) asignan rutas a manejadores (@Get(':id'), @Post()). Devuelves un valor y Nest lo serializa a JSON (200, o 201 para POST). Lee entradas con @Param, @Query, @Body, @Headers, frecuentemente combinado con un pipe (@Param('id', ParseIntPipe)).

Advertencia Inyectar @Res() cambia al modo Express: debes enviar la respuesta tú mismo y pierdes interceptores/@HttpCode. Usa @Res({ passthrough: true }) si solo necesitas establecer una cookie/header.
NestJS 11 Express 5 cambió el routing de wildcards — * desnudo debe ser nombrado: @Get('files/*')@Get('files/*splat').
Cómo decirlo "Los controladores son la capa HTTP delgada — parsean y delegan. El trabajo real vive en los servicios inyectados."

04 · Proveedores e inyección de dependencias

Core: un proveedor es cualquier cosa inyectable (@Injectable() servicios, repositorios, fábricas, valores). Un consumidor declara una dependencia por tipo en su constructor; ese tipo es el token. El módulo registra un proveedor para el token; durante el arranque el contenedor construye el grafo transitivamente e instancia de abajo hacia arriba, cacheando singletons.

Los proveedores personalizados te dan control:

ProveedorUso
useClassResuelve un token a una clase (intercambia por entorno).
useValueInyecta una constante / mock / instancia externa.
useFactoryConstruye dinámicamente (async OK), inyecta dependencias vía inject:[].
useExistingAlias de un token a uno existente (mismo singleton).

Inyecta dependencias no-clase (interfaces, config) vía un token string/symbol + @Inject(TOKEN) — la mecánica de ports-and-adapters.

Cómo decirlo "DI significa que las clases declaran lo que necesitan y el contenedor lo provee. Dependo de interfaces vía tokens para que las implementaciones sean intercambiables y mockeables."

05 · Alcances de inyección y ciclo de vida

Core: tres alcances — DEFAULT (singleton, recomendado), REQUEST (por request), TRANSIENT (por consumidor). Los singletons son seguros porque Node no asigna un hilo por request; solo almacena estado específico de request en REQUEST scope (o mejor, AsyncLocalStorage).

El costo de REQUEST Se propaga hacia arriba — una hoja con REQUEST scope hace que sus consumidores (hasta el controlador) también sean REQUEST-scoped, añadiendo asignación/liberación por request. Un DB/logger compartido con REQUEST scope puede convertir toda la aplicación. Usa proveedores duraderos + una ContextIdStrategy de tenant para recuperar rendimiento.

Hooks del ciclo de vida (orden): onModuleInitonApplicationBootstrap → [ejecutándose] → onModuleDestroybeforeApplicationShutdownonApplicationShutdown. Los hooks de apagado solo se disparan después de app.enableShutdownHooks(); no se ejecutan para proveedores con REQUEST scope.

Cómo decirlo "Por defecto uso singletons y recurre a REQUEST scope solo para estado genuinamente por request — y sé que se propaga, por lo que prefiero AsyncLocalStorage para contexto."

06 · Módulos dinámicos y configuración

Core: un módulo dinámico devuelve sus metadatos desde un método estático para que pueda ser configurado. Convención: register() = por importador, forRoot() = una vez globalmente, forFeature() = ajuste por feature de un forRoot. Cada uno tiene una variante asíncrona (forRootAsync) que acepta useFactory+inject para que las opciones vengan de ConfigService.

La implementación moderna es ConfigurableModuleBuilder, que genera automáticamente la clase base, el token de opciones y las firmas síncronas/asíncronas — eliminando boilerplate escrito a mano.

NestJS 11 Importar el mismo módulo dinámico dos veces con configuración profundamente igual ahora genera instancias separadas — asigna const m = X.forRoot({...}) a una variable y reutilízalo para compartir uno.
Cómo decirlo "forRoot configura un módulo una vez globalmente; forFeature lo ajusta por feature; las variantes asíncronas inyectan config. Uso ConfigurableModuleBuilder para no escribir forRootAsync a mano."

07 · El ciclo de vida de una request

Memoriza esto: Request entrante → MiddlewareGuardsInterceptores (pre)PipesManejador (→ Servicio) → Interceptores (post)Filtros de excepción → Respuesta. Dentro de cada nivel es global → controlador → ruta; los interceptores se desenrollan al salir, y los filtros son el único mejorador que resuelve ruta → controlador → global.

BloqueFunción · ¿tiene ExecutionContext?
MiddlewarePre-routing req/res raw — sin contexto
GuardAutorización — sí (Reflector)
InterceptorAOP antes/después (RxJS) — sí
PipeValida/transforma argumentos — solo metadatos
FilterDa forma a errores — solo ArgumentsHost
Cómo decirlo "Middleware, guards, interceptores, pipes, manejador, interceptores de nuevo, luego filtros en caso de error. Solo guards e interceptores obtienen el ExecutionContext."

08 · Pipes y validación

Core: los pipes (transform(value, metadata)) validan y transforman argumentos del manejador, ejecutándose justo antes del manejador dentro de la zona de excepciones. El ValidationPipe global + class-validator en DTOs de clase es la columna vertebral:

app.useGlobalPipes(new ValidationPipe({ whitelist: true, // elimina props sin decorador forbidNonWhitelisted: true, // lanza error en props desconocidas transform: true, // construye instancia DTO + convierte }));

whitelist/forbidNonWhitelisted defienden contra mass-assignment. Los objetos anidados necesitan @ValidateNested() + @Type(() => Dto). Pipes parse incorporados: ParseIntPipe, ParseUUIDPipe, ParseArrayPipe y ParseDatePipe de v11.

Trampa Los DTOs deben ser clases; import type los elimina y la validación silenciosamente no hace nada.
Cómo decirlo "Un ValidationPipe global con whitelist + transforma valida DTOs de clase y bloquea mass-assignment — la entrada mala se convierte en un 400 antes de que se ejecute mi manejador."

09 · Guards y autorización

Core: un guard (canActivate(ctx) → boolean) decide si una request procede — el hogar de la autorización. Como tiene un ExecutionContext, lee metadatos de ruta vía Reflector:

const roles = this.reflector.getAllAndOverride(ROLES_KEY, [ctx.getHandler(), ctx.getClass()]);

RBAC: @Roles('admin') + un RolesGuard comparando con req.user.roles. Patrón de auth global: registra el JWT guard como APP_GUARD (todo protegido), añade @Public() y cortocircuita en él. Para reglas por recurso escala a ABAC con CASL.

Cómo decirlo "Los guards hacen autorización. Hago las rutas protegidas por defecto vía APP_GUARD con una salida @Public(), y uso el Reflector para verificación de roles."

10 · Interceptores

Core: los interceptores envuelven el manejador en un stream RxJS (intercept(ctx, next) → Observable), dando lógica antes/después alrededor de next.handle(). Omite next.handle() para sobreescribir (caché).

  • Transforman respuestas — map(d => ({ data: d }))
  • Logging/timing — tap(...)
  • Timeouts — timeout(5000) + catchError
  • Serialización — ClassSerializerInterceptor (@Exclude/@Expose)
Cómo decirlo "Los interceptores son AOP: los uso para envolturas de respuesta, timing, timeouts, caché y serialización — cualquier cosa que envuelva el manejador."

11 · Filtros de excepción y manejo de errores

Core: lanza subclases de HttpException (NotFoundException, BadRequestException) y el filtro incorporado de Nest da forma a la respuesta; errores desconocidos → 500. Un filtro personalizado (@Catch() + catch(exception, host)) te permite estandarizar el body de error, loguear y mapear errores de dominio a HTTP.

Patrón AllExceptionsFilter: @Catch() (todo) + inyecta HttpAdapterHost; usa ArgumentsHost para que funcione a través de HTTP/WS/RPC. Extiende BaseExceptionFilter y llama a super.catch() para mantener los predeterminados mientras añades logging.

Cómo decirlo "Lanzo HttpExceptions tipados para fallos esperados y uso un AllExceptionsFilter global para estandarizar la envoltura de error y centralizar el logging."

12 · Middleware

Core: el middleware se ejecuta primero, con req/res/next raw — logging, CORS, helmet, parsing de body, adjuntar un request id. El middleware de clase (NestMiddleware) tiene DI; el funcional no tiene dependencias. Se aplica vía configure(consumer): consumer.apply(LoggerMiddleware).forRoutes('users').

Limitación El middleware no tiene ExecutionContext — no puede conocer el manejador objetivo ni leer sus metadatos. Para cualquier cosa consciente de rutas, usa un guard o interceptor. El middleware global app.use() no puede usar DI.
Cómo decirlo "El middleware es para configuración transversal de requests que no necesita conocer el manejador — logging, helmet, request ids. La lógica consciente de rutas va en guards/interceptores."

13 · Configuración y secretos

Core: @nestjs/config carga .env + fusiona process.env (el entorno real gana). Hazlo global, cachealo y valida al arrancar con Joi/zod para que un despliegue mal configurado crashee inmediatamente en lugar de fallar en el momento de la request. Usa namespaces con registerAs('db', ...) para configuración tipada y modular.

12-factor: la configuración que varía por despliegue vive en el entorno, no en el repo. .env es solo para desarrollo local (gitignored + dockerignored); los secretos de producción vienen de un gestor (k8s Secrets, AWS Secrets Manager, Vault). Node 20+ puede cargar entorno de forma nativa con --env-file.

Cómo decirlo "La configuración se valida al inicio para fallar rápido, los secretos vienen de un gestor no del repo, y uso namespaces en la configuración para type safety."

14 · Bases de datos y el patrón repositorio

Core: Nest integra TypeORM, Prisma y Mongoose. TypeORM: forRoot configura el DataSource, forFeature([Entity]) registra por módulo, inyecta @InjectRepository(User). Prisma: envuelve el cliente generado en un PrismaService (conecta en onModuleInit). Mongoose: @InjectModel sobre clases @Schema.

El patrón repositorio mantiene la aplicación hablando con repositorios, no con el ORM/SQL raw — para que puedas intercambiar el almacén, añadir caché y mapear filas a modelos de dominio en el límite. No filtres entidades (con secretos/relaciones) directamente a respuestas de API.

#1 advertencia TypeORM synchronize: true altera automáticamente el esquema y puede eliminar datos — nunca en producción. Usa migraciones.
Cómo decirlo "Paso por repositorios y mapeo a modelos de dominio en el límite, ejecuto migraciones (nunca synchronize en prod), y mantengo las entidades fuera del contrato de API."

15 · Transacciones e integridad de datos

Core: envuelve escrituras de múltiples pasos en una transacción para que se confirmen o reviertan atómicamente. TypeORM ofrece tres formas: un QueryRunner (más control, debes hacer release() en finally), el callback dataSource.transaction(async mgr => ...) (commit/rollback automático), o el decorador comunitario typeorm-transactional. Cada operación debe compartir el mismo EntityManager o se ejecuta fuera de la transacción.

A través de servicios no puedes usar una transacción de DB — usa el patrón Saga (acciones compensatorias) y el transactional outbox para publicación confiable de eventos, con consumidores idempotentes.

Cómo decirlo "Dentro del servicio, uso una transacción con un EntityManager compartido. Entre servicios no hay 2PC — uso sagas con compensaciones y un outbox para publicación atómica de eventos."

16 · Autenticación (JWT / Passport)

Core: emite un token de acceso de corta duración al iniciar sesión (JwtService.signAsync, id en sub) y vérificalo en un guard en rutas protegidas. Con Passport, un JwtStrategy extrae y valida el token (valor de retorno → req.user) y proteges con AuthGuard('jwt'); sin Passport, un guard hecho a mano usa JwtService directamente.

Combina tokens de acceso con tokens de actualización (rotados, almacenados, revocables) ya que los JWTs no pueden ser revocados antes de expirar. Hashea contraseñas con bcrypt/argon2 (nunca almacenes en texto plano o reversible).

Cómo decirlo "Tokens de acceso JWT de corta duración más tokens de actualización rotativos, verificados en un guard global con una salida @Public(), contraseñas hasheadas con bcrypt/argon2."

17 · Caché

Core: @nestjs/cache-manager (v11 está basado en Keyv) ofrece caché manual (@Inject(CACHE_MANAGER)get/set/del) y caché automática de GET (CacheInterceptor). Patrones: cache-aside (verificar → miss → DB → TTL → invalidar en escritura), read-through, write-through. Nombra stale-while-revalidate para lecturas instantáneas + refresco en background.

A escala El almacén predeterminado es en memoria (por instancia). Usa un almacén Redis (KeyvRedis) para que todas las réplicas compartan la caché, y scope las claves (por ejemplo, por tenant). Protege contra stampede de caché con un lock o TTL con jitter. Los TTLs están en milisegundos; get() devuelve undefined en miss en v11.
Cómo decirlo "Cache-aside con Redis para que sea compartido entre instancias, TTL + invalidación delete-on-write, y protección contra stampede para claves calientes."

18 · Colas y trabajos en background

Core: mueve trabajo pesado/lento fuera de la ruta de request con BullMQ (@nestjs/bullmq, respaldado por Redis). El productor añade trabajos (q.add('name', data, { attempts, backoff, delay, priority })); un @Processor extiende WorkerHost y enruta por job.name en process(). Obtienes reintentos, backoff, delays, prioridades y persistencia duradera.

Idempotencia La entrega es al-menos-una-vez — un trabajo puede ejecutarse dos veces (crash antes del ack). Haz los procesadores idempotentes (clave de deduplicación / upsert), establece timeouts por encima de p99 y enruta reintentos agotados a una DLQ con alertas. En BullMQ, @Process('name') no funciona — usa un switch en job.name.
Cómo decirlo "El trabajo CPU-intensivo o lento va a una cola BullMQ con reintentos y backoff; los consumidores son idempotentes porque la entrega es al-menos-una-vez, y los trabajos muertos caen en una DLQ."

19 · Programación de tareas y eventos

Core: @nestjs/schedule ofrece @Cron(), @Interval(), @Timeout() y un SchedulerRegistry para trabajos dinámicos. @nestjs/event-emitter ofrece pub/sub en proceso (emit / @OnEvent) para desacoplar efectos secundarios del flujo principal.

Trampa multi-réplica Cron se ejecuta por instancia — con N réplicas cada instancia dispara. Usa un lock distribuido o encola un solo trabajo para que se ejecute una vez. El event emitter es síncrono y no duradero — para entrega confiable o entre servicios usa una cola/broker.
Cómo decirlo "Eventos en proceso para desacoplar; un lock distribuido o una cola para trabajo programado para que un cron no se dispare en cada réplica."

20 · Estrategia de testing

Core: @nestjs/testing construye un grafo DI que puedes sobreescribir. Unitario: Test.createTestingModule({...}).overrideProvider(X).useValue(mock).compile(), luego module.get() (o resolve() para scoped). E2E: createNestApplication()init() → impulsa con supertestapp.close().

La pirámide: muchas pruebas unitarias rápidas (mock de colaboradores) → menos pruebas de integración (DB/Redis real vía Testcontainers) → algunas e2e. Prueba los cinco resultados de un flujo: respuesta, cambio de DB, llamada saliente, mensaje encolado, observabilidad.

NestJS 12 Vitest está programado para reemplazar a Jest como el runner predeterminado — los patrones (createTestingModule, overrides, supertest) se mantienen iguales.
Cómo decirlo "Mayormente pruebas unitarias rápidas con proveedores sobrescritos, pruebas de integración contra Postgres/Redis real vía Testcontainers, y una capa delgada de e2e con supertest."