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.
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.
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
providersde 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.
@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)).
@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.* desnudo debe ser nombrado: @Get('files/*') → @Get('files/*splat').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:
| Proveedor | Uso |
|---|---|
useClass | Resuelve un token a una clase (intercambia por entorno). |
useValue | Inyecta una constante / mock / instancia externa. |
useFactory | Construye dinámicamente (async OK), inyecta dependencias vía inject:[]. |
useExisting | Alias 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.
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).
ContextIdStrategy de tenant para recuperar rendimiento.Hooks del ciclo de vida (orden): onModuleInit → onApplicationBootstrap → [ejecutándose] → onModuleDestroy → beforeApplicationShutdown → onApplicationShutdown. Los hooks de apagado solo se disparan después de app.enableShutdownHooks(); no se ejecutan para proveedores con REQUEST scope.
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.
const m = X.forRoot({...}) a una variable y reutilízalo para compartir uno.07 · El ciclo de vida de una request
Memoriza esto: Request entrante → Middleware → Guards → Interceptores (pre) → Pipes → Manejador (→ 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.
| Bloque | Función · ¿tiene ExecutionContext? |
|---|---|
| Middleware | Pre-routing req/res raw — sin contexto |
| Guard | Autorización — sí (Reflector) |
| Interceptor | AOP antes/después (RxJS) — sí |
| Pipe | Valida/transforma argumentos — solo metadatos |
| Filter | Da forma a errores — solo ArgumentsHost |
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:
whitelist/forbidNonWhitelisted defienden contra mass-assignment. Los objetos anidados necesitan @ValidateNested() + @Type(() => Dto). Pipes parse incorporados: ParseIntPipe, ParseUUIDPipe, ParseArrayPipe y ParseDatePipe de v11.
import type los elimina y la validación silenciosamente no hace nada.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:
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.
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)
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.
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').
app.use() no puede usar DI.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.
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.
synchronize: true altera automáticamente el esquema y puede eliminar datos — nunca en producción. Usa migraciones.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.
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).
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.
get() devuelve undefined en miss en v11.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.
@Process('name') no funciona — usa un switch en job.name.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.
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 supertest → app.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.