Parte VII · Práctica

23. Banco de preguntas de entrevista y simulacros

Este capítulo recoge las preguntas que de verdad se hacen en las entrevistas de este stack, con respuestas al nivel que se espera de cada categoría profesional y, sobre todo, con las repreguntas que vienen después, que es donde se decide la mayoría de los procesos. No es un cuestionario para memorizar: es un mapa de lo que el entrevistador quiere averiguar sobre ti.

CORE Tiempo de lectura: ~120 min Prerrequisitos: todo el libro

23.1 Cómo usar este banco

Hay una forma de aprovechar estas preguntas que funciona y otra que no. La que no funciona es leerlas seguidas asintiendo: genera una sensación de dominio que se evapora en cuanto alguien te mira a la cara y espera una respuesta hablada. La que funciona es taparse la respuesta, contestar en voz alta, y solo entonces comparar. La diferencia entre reconocer una respuesta y ser capaz de producirla es enorme, y es justo la que mide una entrevista.

23.1.1 El método para responder

  ESTRUCTURA DE UNA BUENA RESPUESTA (30-90 segundos)

  1. DEFINICIÓN         Una frase precisa.       "Un guard decide si una ruta
                                                  puede activarse."
  2. PROBLEMA           Por qué existe.          "Evita duplicar la comprobación
                                                  de permisos en cada componente."
  3. EJEMPLO CONCRETO   De algo que has hecho.   "En TaskFlow lo usamos para..."
  4. MATIZ              Lo que distingue         "Ojo: no es seguridad real; el
                        al que lo ha usado        backend debe volver a validar."
                        del que lo ha leído.

  Luego CÁLLATE y deja que repregunten. Rellenar el silencio con divagaciones
  es la forma más común de estropear una respuesta que ya era correcta.
Qué hacer cuando no sabes algo Decir «no lo sé» con naturalidad y añadir cómo lo averiguarías puntúa mejor que improvisar. Un entrevistador con experiencia detecta el farol en dos repreguntas, y a partir de ahí duda de todo lo que has dicho antes, incluido lo que sí sabías. La fórmula que funciona: «No lo he usado en producción. Sé que sirve para X, y si me tocara implementarlo empezaría por Y y comprobaría Z en la documentación». Eso demuestra honestidad y criterio, que es lo que se busca en alguien que va a trabajar contigo durante años.

23.1.2 Qué evalúa realmente el entrevistador

Lo que parece que se evalúaLo que se evalúa de verdad
Si te sabes la definiciónSi entiendes qué problema resuelve y cuándo no usarlo
Si conoces la última versiónSi sabes distinguir lo que cambia de lo que permanece
Si respondes rápidoSi preguntas antes de responder cuando el enunciado es ambiguo
Si aciertas el ejercicioCómo razonas cuando te atascas y si aceptas una pista
Cuánta tecnología dominasSi serías agradable de tener al lado durante dos años

23.1.3 Plan de preparación según el puesto

23.2 JavaScript y TypeScript

Junior ¿Qué diferencia hay entre == y ===?
=== compara valor y tipo sin conversiones. == aplica coerción de tipos antes de comparar, siguiendo unas reglas que producen resultados sorprendentes: '' == 0, '1' == 1 y null == undefined son todos true, mientras que NaN == NaN es false. La norma es usar siempre ===. La única excepción razonablemente extendida es x == null para comprobar de una vez null y undefined, aunque hoy suele preferirse x ?? valor. Repregunta habitual: ¿y Object.is? Es como === salvo en dos casos: distingue +0 de -0 y considera que NaN es igual a NaN.
Junior Explica var, let y const.
var tiene ámbito de función y se eleva inicializada a undefined, lo que permite usarla antes de declararla sin error. let y const tienen ámbito de bloque y también se elevan, pero quedan en la zona muerta temporal: acceder a ellas antes de la declaración lanza ReferenceError, que es mucho mejor que un undefined silencioso. const impide reasignar la variable, no mutar el objeto al que apunta: puedes hacer const a = []; a.push(1); sin problema. La práctica recomendada es const por defecto, let cuando necesites reasignar y var nunca.
Medio ¿Qué imprime este código y por qué?
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
queueMicrotask(() => console.log('4'));
console.log('5');
Imprime 1, 5, 3, 4, 2. Primero se ejecuta todo el código síncrono (1 y 5). Al vaciarse la pila, el motor procesa toda la cola de microtareas, donde están la continuación de la promesa (3) y el queueMicrotask (4), en orden de encolado. Solo después se atiende la cola de macrotareas, donde espera el setTimeout (2), aunque su retardo sea cero. La regla que hay que interiorizar es que las microtareas se vacían enteras antes de pasar a la siguiente macrotarea. Repregunta habitual: ¿qué pasa si una microtarea encola otra microtarea? Se ejecuta también en esa misma tanda; un bucle de microtareas que se reencolan bloquea el hilo indefinidamente y el navegador nunca llega a repintar.
Medio ¿Qué es un closure y para qué se usa?
Un closure es una función que conserva acceso a las variables del ámbito donde fue creada, incluso después de que ese ámbito haya terminado. Se usa para encapsular estado privado (un contador, una caché, una configuración), para crear funciones parcialmente aplicadas y en todo tipo de fábricas.
function crearContador() {
  let n = 0;                       // vive mientras viva la función devuelta
  return { incrementar: () => ++n, valor: () => n };
}
El coste es que esas variables no se liberan mientras exista una referencia a la función, de ahí que los closures sean una causa habitual de fugas de memoria cuando capturan objetos grandes o nodos del DOM. Repregunta clásica: el bucle con var y setTimeout que imprime el mismo número tres veces. Con var hay una sola variable compartida; con let se crea un enlace nuevo por iteración y funciona como se espera.
Medio ¿Cómo se determina el valor de this?
En una función normal, this depende de cómo se llama: como método de un objeto es ese objeto; suelta, es undefined en modo estricto (y el objeto global fuera de él); con new, la instancia nueva; y con call, apply o bind, lo que le indiques. Las funciones flecha son la excepción importante: no tienen this propio, lo toman léxicamente del ámbito donde se definieron y no se puede cambiar ni con bind. Por eso son la opción correcta para callbacks dentro de una clase, y la incorrecta para definir un método de un objeto literal que necesite acceder a sus hermanos. Repregunta habitual: ¿por qué se pierde this al pasar un método como callback? Porque se pasa la función desligada del objeto; se soluciona con bind o envolviéndola en una flecha.
Medio ¿Qué es la cadena de prototipos?
Cada objeto tiene una referencia interna a otro objeto, su prototipo. Cuando accedes a una propiedad que el objeto no tiene, el motor la busca en su prototipo, luego en el prototipo de este, y así hasta llegar a null. Eso es la herencia en JavaScript: delegación, no copia. Las clases de ES6 son azúcar sintáctico sobre este mecanismo, no un sistema de clases real como el de Java. Consecuencia práctica: los métodos definidos en una clase viven en el prototipo y se comparten entre todas las instancias, mientras que las propiedades de campo se crean una por instancia.
Junior ¿Copia superficial o profunda?
Una copia superficial duplica el primer nivel: {...obj} y Object.assign copian las referencias de los objetos anidados, así que mutar copia.direccion.calle también cambia el original. Una copia profunda duplica todo el árbol; hoy la forma nativa es structuredClone(obj), que además maneja Map, Set, Date y referencias circulares, cosa que el viejo truco de JSON.parse(JSON.stringify(x)) no hace (y que además destruye fechas, convierte undefined en ausencia y falla con funciones). En una aplicación con estado inmutable, la copia superficial por niveles suele ser preferible por rendimiento.
Medio ¿Promise.all, allSettled, race o any?
all espera a todas y falla en cuanto una falla, descartando el resto de resultados: úsalo cuando necesites todas las respuestas para continuar. allSettled espera a todas y te devuelve el estado de cada una: es el correcto cuando quieres el máximo de resultados posible, por ejemplo al cargar varios widgets independientes de un panel. race se resuelve o rechaza con la primera que termine, sea cual sea: sirve para implementar tiempos límite. any devuelve la primera que tenga éxito e ignora los fallos, y solo rechaza si fallan todas. Repregunta habitual: ¿Promise.all cancela las peticiones restantes cuando una falla? No. Las promesas no son cancelables; las peticiones siguen su curso y solo se ignora su resultado. Para cancelar de verdad hace falta AbortController.
Medio ¿Diferencia entre interface y type?
Se solapan mucho. Las interface admiten fusión de declaraciones (si declaras dos con el mismo nombre se combinan), lo que es útil para ampliar tipos de librerías de terceros, y suelen dar mensajes de error algo más legibles. Los type pueden expresar cosas que una interfaz no: uniones, intersecciones, tuplas, tipos condicionales y mapeados. El criterio práctico habitual es usar interface para la forma de un objeto o el contrato de una clase, y type para todo lo demás. Lo importante en la entrevista es que expliques el criterio, no que recites la lista.
Medio ¿any, unknown o never?
any desactiva el sistema de tipos: puedes hacer cualquier cosa con ese valor y el compilador calla, incluido propagar el any a todo lo que toque. unknown es su alternativa segura: acepta cualquier valor, pero no te deja usarlo hasta que compruebes de qué tipo es. Es el tipo correcto para lo que llega del exterior (respuestas de red, JSON.parse, el catch de un try). never representa lo que no puede ocurrir: el tipo de retorno de una función que siempre lanza, y la herramienta para conseguir comprobaciones exhaustivas en un switch, porque si añades un caso al union y olvidas tratarlo, la asignación a never deja de compilar.
Medio ¿Qué son los guardas de tipo?
Son comprobaciones que estrechan el tipo dentro de un bloque. Las integradas son typeof, instanceof, in y la comparación con literales. Además puedes escribir predicados propios con la firma x is Tipo:
function esTarea(x: unknown): x is Task {
  return typeof x === 'object' && x !== null && 'title' in x;
}
El matiz importante que se espera de un perfil senior: un predicado así es una promesa que haces al compilador, no una validación que él verifique. Si mientes, el error aparecerá en tiempo de ejecución igualmente. Para datos externos es más seguro validar con un esquema (Zod, class-validator) que además infiere el tipo.
Medio Nombra tipos de utilidad y para qué sirven.
Partial<T> hace todo opcional (típico en una actualización parcial), Required<T> lo contrario, Readonly<T> impide reasignar propiedades, Pick<T,K> y Omit<T,K> seleccionan o excluyen claves, Record<K,V> construye un diccionario tipado, ReturnType<F> y Parameters<F> extraen tipos de una función, Awaited<T> desenvuelve una promesa, y NonNullable<T> elimina null y undefined. En NestJS verás sus equivalentes específicos (PartialType, PickType) que además conservan los metadatos de validación, cosa que los de TypeScript no hacen porque desaparecen al compilar.
Senior ¿Qué aporta satisfies?
Comprueba que una expresión cumple un tipo sin ensanchar el tipo inferido. Con una anotación normal pierdes la información concreta:
const rutas = { home: '/', tareas: '/tasks' } satisfies Record<string, string>;
rutas.home;      // tipo '/'  ← literal conservado
// Con  const rutas: Record<string, string>  sería  string,
// y además se perderían los nombres exactos de las claves.
Es especialmente útil en objetos de configuración y mapas de constantes, donde quieres validar la forma pero conservar los literales para el autocompletado y para los tipos derivados.
Senior Explica las uniones discriminadas.
Es una unión de tipos que comparten una propiedad literal que actúa de etiqueta, lo que permite al compilador saber exactamente en qué rama estás:
type Estado =
  | { tipo: 'cargando' }
  | { tipo: 'ok'; datos: Task[] }
  | { tipo: 'error'; mensaje: string };

if (estado.tipo === 'ok') estado.datos;   // aquí datos existe y está tipado
Es el patrón correcto para modelar el estado de una vista, porque hace imposible representar combinaciones absurdas como «cargando y con error a la vez», que es justo lo que ocurre cuando usas tres booleanos sueltos. Combinado con una comprobación exhaustiva sobre never, el compilador te obliga a tratar cualquier estado nuevo que añadas.
Senior ¿Cómo funcionan los decoradores y por qué los usan Angular y NestJS?
Un decorador es una función que se aplica a una clase, método, propiedad o parámetro y puede observarlos o modificarlos. En este stack se usan sobre todo para adjuntar metadatos: @Component, @Injectable, @Controller o @Entity registran información que el framework lee después para construir el grafo de dependencias, las rutas o el mapeo de la base de datos. La pieza que lo hace posible en tiempo de ejecución es reflect-metadata junto con emitDecoratorMetadata, que permite conocer los tipos de los parámetros del constructor y así resolver la inyección por tipo. Matiz importante: existen los decoradores «legacy» de TypeScript (experimentales) y los estandarizados de ECMAScript, que no son idénticos; los frameworks han ido migrando y conviene saber cuál usa tu proyecto.
Medio ¿Qué hace strictNullChecks?
Sin esa opción, null y undefined pertenecen a todos los tipos, así que el compilador te deja llamar a un método sobre algo que puede no existir. Con ella activada, esos dos valores solo son asignables a tipos que los incluyan explícitamente, y estás obligado a comprobar antes de usar. Es, con diferencia, la opción que más errores previene en tiempo de ejecución: elimina de raíz la familia entera de Cannot read properties of undefined. Forma parte de strict, que es lo que deberías tener activado en cualquier proyecto nuevo.
Senior ¿Qué es el tipado estructural?
TypeScript compara los tipos por su forma, no por su nombre: si un objeto tiene las propiedades que exige un tipo, es compatible con él aunque no lo declare. Es lo contrario del tipado nominal de Java o C#. Ventaja: enorme flexibilidad, no necesitas implementar una interfaz para satisfacerla. Inconveniente: dos tipos conceptualmente distintos con la misma forma son intercambiables, de modo que puedes pasar un UserId donde se espera un ProjectId si ambos son string. La solución habitual son los tipos de marca: type UserId = string & { readonly __marca: unique symbol }.
Medio ¿Qué significa que los tipos se borran al compilar?
Que en el JavaScript resultante no queda ni rastro de ellos: no puedes preguntar por un tipo en tiempo de ejecución, no existe if (x instanceof MiInterfaz) y los genéricos no están disponibles dentro de la función. De ahí que la validación de datos externos necesite código real (Zod, class-validator) y no baste con anotar el tipo de la respuesta HTTP: escribir http.get<Task[]>(...) no comprueba nada, solo le dice al compilador «confía en mí». Es un malentendido muy frecuente y una pregunta excelente para separar a quien ha leído TypeScript de quien lo ha sufrido.
Junior ¿Diferencia entre null y undefined?
undefined es la ausencia por defecto: una variable declarada sin valor, una propiedad que no existe o el retorno de una función sin return. null es una ausencia asignada deliberadamente: «aquí no hay nada, y lo sé». En la práctica conviene elegir una convención de equipo y respetarla; muchos proyectos usan undefined internamente y reservan null para lo que viene de la base de datos, donde tiene significado propio. El operador ?? y el encadenamiento opcional ?. tratan los dos igual, que suele ser lo que quieres.
Medio ¿Qué diferencia hay entre map, forEach, reduce y filter?
map transforma cada elemento y devuelve un array nuevo del mismo tamaño; filter devuelve un subconjunto; reduce colapsa el array en un único valor, que puede ser un número, un objeto o incluso otro array; forEach no devuelve nada y solo sirve para efectos secundarios. El error habitual es usar forEach con un push a un array externo donde tocaba un map, o encadenar cinco filter y map sobre listas grandes creando arrays intermedios innecesarios. Y una trampa clásica: forEach no espera a las funciones asíncronas; para eso se usa un for...of con await, o Promise.all con map si quieres paralelismo.
Medio ¿ESM o CommonJS?
CommonJS (require) es el sistema histórico de Node: síncrono, dinámico y resuelto en tiempo de ejecución. ESM (import) es el estándar del lenguaje: estático, lo que permite analizar las dependencias sin ejecutar el código y por tanto habilita el tree shaking que elimina lo que no usas del bundle. Hoy el frontend es ESM sin discusión; en el backend la migración lleva años porque muchos paquetes y herramientas asumen CommonJS. Los puntos de fricción típicos: no puedes usar require en ESM, __dirname no existe, y hay que respetar las extensiones en las rutas de importación.
Senior ¿Qué es un genérico y cuándo lo usarías?
Un parámetro de tipo que permite escribir código reutilizable sin perder información de tipos. La señal de que hace falta uno es que estés a punto de usar any para que una función acepte varios tipos, o que tengas la misma función duplicada para Task y para Project. Con restricciones (extends) puedes exigir que el tipo cumpla algo:
function porId<T extends { id: number }>(items: T[], id: number): T | undefined {
  return items.find(i => i.id === id);   // devuelve el tipo CONCRETO, no un genérico vago
}
El error opuesto también existe: genéricos innecesarios con cuatro parámetros de tipo que nadie entiende. Si un parámetro de tipo se usa una sola vez, probablemente sobra.
Senior ¿Cómo evitas bloquear el event loop en Node?
Node ejecuta tu JavaScript en un solo hilo, así que cualquier operación síncrona larga congela todas las peticiones, no solo la actual. Los sospechosos habituales: bucles sobre colecciones enormes, JSON.parse de cargas de megabytes, criptografía síncrona, expresiones regulares con retroceso catastrófico y las variantes ...Sync de las funciones de sistema de archivos. Las soluciones, por orden: mover el trabajo a la base de datos, trocearlo en lotes cediendo el control entre ellos, delegarlo a worker_threads si es cálculo puro, o sacarlo del proceso a una cola de trabajos. Para diagnosticarlo se mide el retraso del event loop, que es la métrica que hay que vigilar en producción.
Medio ¿Qué es async/await por debajo?
Azúcar sintáctico sobre promesas y generadores. Una función async siempre devuelve una promesa, y cada await suspende la ejecución encolando el resto de la función como una microtarea que se reanudará cuando la promesa se resuelva. Consecuencias prácticas: los await secuenciales sobre operaciones independientes desperdician tiempo (deberían ir en un Promise.all), await dentro de un bucle convierte N operaciones paralelas en N secuenciales, y un error dentro de una función asíncrona se convierte en un rechazo de la promesa, así que un try/catch síncrono alrededor de la llamada no lo captura si olvidas el await.
Senior ¿Qué es una promesa flotante y por qué importa?
Una promesa que se crea y nadie espera ni maneja. Si falla, en Node moderno provoca un rechazo no capturado que puede tumbar el proceso, y en cualquier caso pierdes el orden de ejecución: el código continúa sin que la operación haya terminado. Es una de las fuentes más comunes de errores sutiles en backend («a veces el registro no se guarda»). La regla ESLint @typescript-eslint/no-floating-promises las detecta todas y debería estar activada en cualquier proyecto serio. Si de verdad quieres lanzar algo sin esperarlo, hazlo explícito con void miPromesa() y un manejador de errores.

23.3 Angular

Junior ¿Qué es un componente standalone y qué problema resolvió?
Es un componente que declara sus propias dependencias en imports en lugar de pertenecer a un NgModule. Resolvió un problema real de ergonomía: con módulos, para usar un componente había que declararlo en un módulo, exportarlo, importar ese módulo donde lo necesitaras, y cualquier error daba mensajes crípticos. Además, el módulo era una capa de indirección que no aportaba información: para saber qué usa un componente había que abrir otro archivo. Con standalone, las dependencias están donde se usan, el árbol de dependencias es más fácil de analizar y el tree shaking funciona mejor.
Junior Enumera los hooks del ciclo de vida y para qué sirve cada uno.
En orden: ngOnChanges (cambió una entrada; recibe los valores anterior y actual), ngOnInit (una vez, tras la primera asignación de entradas: el sitio correcto para inicializar), ngDoCheck (detección personalizada; caro, se ejecuta constantemente), ngAfterContentInit y ngAfterContentChecked (contenido proyectado listo), ngAfterViewInit y ngAfterViewChecked (vista e hijos listos: aquí ya puedes medir el DOM) y ngOnDestroy (limpieza de suscripciones, temporizadores y escuchas). El matiz que se espera: el constructor solo debe inyectar y asignar, porque en él las entradas todavía no tienen valor. Repregunta habitual: ¿por qué modificar estado en ngAfterViewInit da ExpressionChangedAfterItHasBeenChecked? Porque cambias un valor que Angular ya había comprobado en ese mismo ciclo; en desarrollo hace una segunda pasada de verificación y detecta la discrepancia.
Medio ¿Cómo funciona la detección de cambios?
Angular recorre el árbol de componentes de arriba abajo comparando el valor actual de cada expresión de la plantilla con el de la comprobación anterior, y actualiza el DOM donde haya diferencias. Históricamente, lo que dispara ese recorrido es Zone.js, que parchea las API asíncronas del navegador (eventos, temporizadores, peticiones) para avisar a Angular de que «algo ha podido cambiar». Con Default se revisa todo el árbol; con OnPush, solo se revisa un componente si cambia la referencia de alguna entrada, si se emite un evento desde su plantilla, si se marca explícitamente o si una señal que consume cambia de valor. Repregunta de nivel senior: ¿qué cambia con el modo sin zona? Que Angular deja de depender de Zone.js y se apoya en las señales para saber exactamente qué hay que revisar, lo que reduce el trabajo y el tamaño del bundle.
Medio ¿Señales o RxJS? ¿Cuándo cada uno?
No compiten en el mismo terreno. Una señal es un valor que siempre tiene contenido, se lee de forma síncrona y notifica a quien depende de él: es ideal para el estado de la interfaz y para los valores derivados. Un observable es un flujo de eventos en el tiempo, con herramientas para componer concurrencia: cancelación, reintentos, debounce, combinación de fuentes. La regla práctica: señales para el estado que se pinta, RxJS para orquestar eventos asíncronos complejos, y conversión en la frontera con toSignal y toObservable. Un buscador con debounce y cancelación es RxJS; el resultado que se muestra es una señal.
Medio ¿computed o effect?
computed deriva un valor de otras señales: es puro, perezoso (no se calcula si nadie lo lee) y memoizado. effect ejecuta un efecto secundario cuando cambian sus dependencias: escribir en el almacenamiento local, cambiar el título del documento, llamar a una librería externa. El error más común, y una pregunta trampa habitual, es usar un effect para escribir en otra señal: eso crea un grafo reactivo difícil de seguir y propenso a bucles, y casi siempre significa que lo que querías era un computed. La regla: si el resultado es un valor, computed; si el resultado es una acción sobre el mundo exterior, effect.
Senior ¿Para qué sirve linkedSignal?
Para un estado que normalmente deriva de otro pero que el usuario puede sobrescribir, y que debe reiniciarse cuando la fuente cambia. El caso típico: un desplegable cuya opción seleccionada depende de la lista de opciones cargada; si la lista cambia, la selección debe volver al valor por defecto, pero mientras tanto el usuario puede elegir otra. Con un computed no puedes escribir; con una señal normal tienes que sincronizarla a mano con un efecto, que es justo el patrón frágil que queremos evitar. Conviene comprobar la disponibilidad exacta de esta API en la versión de Angular del proyecto, porque es reciente.
Medio ¿Por qué es importante track en @for?
Le dice a Angular cómo identificar cada elemento de la lista entre renderizados. Sin una identidad estable, al cambiar la colección Angular destruye y recrea nodos del DOM que en realidad son los mismos, lo que cuesta rendimiento y, peor aún, pierde el estado interno de esos nodos: el foco, el texto que el usuario estaba escribiendo, la posición del scroll o una animación a medias. En el control de flujo nativo track es obligatorio precisamente por eso. Usa el identificador de negocio; usar el índice es correcto solo si la lista nunca se reordena ni se filtra.
Medio ¿Qué es @defer y cuándo lo usarías?
Es la carga diferida a nivel de plantilla: el bloque y sus dependencias se descargan en un paquete aparte y solo cuando se cumple un disparador (on viewport, on interaction, on idle, on timer, when con una condición). Admite bloques @placeholder, @loading y @error. Es la herramienta directa para reducir el JavaScript inicial: un editor de texto enriquecido, un mapa o un gráfico que están por debajo del pliegue no tienen por qué entrar en el bundle inicial. El matiz: no abuses, porque cada bloque diferido es una petición adicional y un posible salto de diseño si el marcador de posición no reserva el mismo espacio.
Medio ¿Cómo funciona la inyección de dependencias en Angular?
Angular mantiene un árbol de inyectores en paralelo al árbol de componentes. Al pedir una dependencia, la busca en el inyector del elemento actual y, si no está, sube hacia los inyectores padre, luego al de la ruta, al de entorno y finalmente al raíz; si nadie la proporciona, lanza NullInjectorError. providedIn: 'root' registra un servicio único para toda la aplicación y permite eliminarlo del bundle si nadie lo usa. Registrarlo en el array providers de un componente crea una instancia por cada instancia de ese componente, lo que sirve para estado local aislado. Los modificadores @Optional, @Self, @SkipSelf y @Host alteran esa búsqueda.
Medio ¿inject() o inyección por constructor?
Funcionalmente equivalentes en un componente o servicio, pero inject() permite cosas que el constructor no: usarlo en inicializadores de campo, escribir funciones auxiliares reutilizables que inyectan por su cuenta, y es la única forma en guards, resolvers e interceptores funcionales. También evita la verbosidad de los constructores con ocho parámetros y funciona sin emitDecoratorMetadata. La restricción a recordar: solo puede llamarse dentro de un contexto de inyección (constructor, inicializador de campo o dentro de runInInjectionContext); llamarlo dentro de ngOnInit o de un callback da error.
Senior ¿Por qué no se puede inyectar una interfaz y qué se hace en su lugar?
Porque las interfaces de TypeScript desaparecen al compilar, así que no existe ningún valor en tiempo de ejecución que sirva de clave para el inyector. La solución es un InjectionToken tipado, que sí es un objeto real y conserva el tipo para el compilador:
export const PASARELA_PAGO = new InjectionToken<PasarelaPago>('PasarelaPago');
// providers: [{ provide: PASARELA_PAGO, useClass: PasarelaStripe }]
Este patrón es la base de la inversión de dependencias en Angular: el componente depende del token (la abstracción) y la configuración decide la implementación, lo que permite sustituirla en tests o por entorno.
Medio ¿Diferencia entre viewChild y contentChild?
viewChild busca en la plantilla propia del componente, la que tú escribiste en su template. contentChild busca en el contenido que el padre le ha proyectado mediante ng-content. La confusión es habitual porque visualmente están en el mismo sitio, pero pertenecen a componentes distintos y sus hooks de ciclo de vida son diferentes: ngAfterContentInit ocurre antes que ngAfterViewInit. En las versiones recientes existen las variantes basadas en señales, que son más cómodas porque no dependen del momento exacto del ciclo de vida para tener valor.
Medio ¿Formularios reactivos o template-driven?
Reactivos en prácticamente cualquier formulario no trivial: el modelo está en el TypeScript, es tipado, testeable sin renderizar, y permite validación dinámica, campos condicionales, FormArray y composición. Template-driven es más rápido de escribir para un formulario de dos campos y resulta cómodo para prototipos, pero la lógica queda repartida en la plantilla y probarla exige montar el componente. En una entrevista, la respuesta que puntúa es explicar el criterio de elección, no declarar uno «mejor».
Medio ¿Cómo se implementa un validador asíncrono correctamente?
Devolviendo un observable o promesa que emite null si es válido o un objeto de error si no. Tres detalles que separan una implementación buena de una mala: se registra en el tercer argumento del control, no junto a los síncronos; hay que aplicar debounceTime y switchMap para no lanzar una petición por tecla ni procesar respuestas obsoletas; y el control pasa por el estado pending mientras se resuelve, que es lo que debes usar para mostrar un indicador y deshabilitar el envío. Ah, y la validación en el servidor sigue siendo obligatoria: esto es comodidad para el usuario, no seguridad.
Medio ¿Qué es un guard funcional y cuáles existen?
Una función que se ejecuta antes de activar o abandonar una ruta y devuelve boolean, UrlTree (para redirigir) o un observable o promesa de estos. canActivate protege la entrada; canActivateChild, las rutas hijas; canDeactivate, la salida (el clásico «tienes cambios sin guardar»); canMatch decide si la ruta siquiera coincide, lo que permite tener dos rutas con el mismo camino para perfiles distintos y, muy importante, evita descargar el bundle de una ruta a la que el usuario no tiene acceso. El matiz de seguridad que hay que decir siempre: un guard es experiencia de usuario, no seguridad; cualquiera puede saltárselo desde la consola del navegador y el backend debe validar igualmente.
Medio ¿Qué hace un interceptor y qué casos típicos cubre?
Intercepta todas las peticiones y respuestas del HttpClient, permitiendo modificarlas o reaccionar a ellas de forma centralizada. Casos habituales: añadir la cabecera de autorización, refrescar el token al recibir un 401 y reintentar, registrar tiempos, mostrar un indicador global de carga, reintentar errores de red, transformar errores a un formato de dominio y añadir un identificador de correlación. Los interceptores funcionales se registran con withInterceptors([...]) y se ejecutan en el orden en que se declaran, en forma de cebolla: la petición atraviesa todos hacia fuera y la respuesta los recorre en sentido inverso.
Medio ¿Cómo se evitan las fugas de memoria por suscripciones?
Cuatro estrategias, de mejor a peor: usar la tubería async en la plantilla, que se suscribe y cancela sola; convertir a señal con toSignal, que se limpia con el contexto de inyección; usar takeUntilDestroyed() en el flujo; o guardar la suscripción y cancelarla en ngOnDestroy. Conviene saber que no todas las suscripciones fugan: las de HttpClient se completan solas tras la respuesta. Las peligrosas son las de flujos infinitos: eventos del DOM, temporizadores, WebSockets, y observables de estado compartido.
Senior Explica switchMap, mergeMap, concatMap y exhaustMap.
Los cuatro aplanan un observable de observables, pero gestionan la concurrencia de forma distinta y elegir mal es una fuente clásica de errores. switchMap cancela el interior anterior al llegar uno nuevo: perfecto para búsquedas y navegación, peligroso para guardar datos porque puede cancelar una escritura a medias. mergeMap los ejecuta todos en paralelo sin garantizar orden: bien para operaciones independientes. concatMap los encola y respeta el orden: el correcto para una secuencia de escrituras. exhaustMap ignora los nuevos mientras haya uno en curso: la respuesta perfecta al doble clic en un botón de envío.
Medio ¿Qué es la carga diferida de rutas y qué gano?
Cargar el código de una sección solo cuando el usuario navega a ella, mediante loadComponent o loadChildren con un import() dinámico. El beneficio es directo sobre el tiempo hasta que la aplicación es utilizable: el usuario que solo entra a ver su lista de tareas no descarga el panel de administración ni el generador de informes. Se puede afinar con estrategias de precarga, que descargan en segundo plano los bundles probables cuando la red está ociosa, y con canMatch para no descargar siquiera lo que el usuario no puede ver.
Senior ¿Qué es la hidratación y qué problemas da?
Tras el renderizado en servidor, el navegador recibe HTML ya pintado; la hidratación es el proceso por el que Angular toma ese DOM existente y lo «adopta» asociándole sus componentes y escuchas, en lugar de borrarlo y volver a crearlo. Sin ella se produce el parpadeo característico de la doble renderización. Los problemas clásicos son las discrepancias entre lo que generó el servidor y lo que espera el cliente: fechas formateadas con distinta zona horaria, valores aleatorios, acceso directo a window, HTML inválido que el navegador reestructura, o manipulación directa del DOM por librerías de terceros. La hidratación incremental permite además retrasar la activación de partes de la página hasta que se necesiten.
Senior ¿Para qué sirve TransferState?
Para que los datos que el servidor ya pidió al renderizar viajen dentro del HTML y el cliente no vuelva a pedirlos nada más arrancar. Sin él, el usuario ve el contenido renderizado y acto seguido la aplicación lanza otra vez las mismas peticiones, con el consiguiente parpadeo y carga inútil en el servidor. En Angular moderno, la integración con HttpClient hace esto de forma bastante automática al activar la hidratación. Cuidado con dos cosas: no metas datos sensibles ahí, porque acaban en el HTML visible para cualquiera, y vigila el tamaño, porque infla el documento.
Medio ¿Cómo gestionas el estado en una aplicación Angular grande?
Por capas, de menos a más: estado local del componente con señales; estado compartido entre unas pocas vistas en un servicio con señales o un pequeño store; y solo cuando el dominio lo justifica, una solución formal como NgRx o un SignalStore, que aportan trazabilidad, herramientas de depuración con viaje en el tiempo y un flujo unidireccional estricto a cambio de bastante ceremonia. Un punto que impresiona en una entrevista: distinguir el estado de servidor (datos que son una caché de la base de datos, con sus problemas de invalidación y frescura) del estado de interfaz (qué pestaña está abierta), porque son problemas distintos y mezclarlos es lo que hace que los stores se conviertan en un vertedero.
Medio ¿Qué es el modelo contenedor/presentación?
Separar los componentes que obtienen datos (contenedores: inyectan servicios, conocen el enrutado, orquestan) de los que solo los muestran (presentación: reciben entradas, emiten salidas, no inyectan nada). El beneficio es que los de presentación son triviales de probar y de reutilizar, y se pueden desarrollar de forma aislada. Además simplifica el rendimiento, porque los de presentación son candidatos naturales a OnPush. No hay que llevarlo al extremo religioso: un componente pequeño que inyecta un servicio no es un pecado.
Senior ¿Cómo depurarías una aplicación Angular lenta?
Primero medir, nunca adivinar. Con el Profiler de Angular DevTools grabas la interacción lenta y ves qué componentes se revisan y cuántas veces; con la pestaña Performance del navegador distingues si el tiempo se va en JavaScript, en layout o en la red; y con Lighthouse obtienes las métricas de experiencia. Las causas más frecuentes, por orden: funciones o getters caros invocados desde la plantilla en cada ciclo, listas sin track correcto, ausencia de OnPush, componentes que se redibujan por eventos de alta frecuencia como scroll o mousemove, bundles enormes sin carga diferida e imágenes sin optimizar. La respuesta que distingue a un senior incluye siempre «y mido otra vez después para demostrar la mejora».
Medio ¿Cómo protege Angular frente a XSS?
Sanea automáticamente todo lo que se interpola en la plantilla: si asignas HTML con etiquetas peligrosas a [innerHTML], elimina los scripts y los atributos de evento. La brecha se abre cuando alguien usa bypassSecurityTrustHtml u otro método del DomSanitizer con contenido que viene del usuario, o cuando se manipula el DOM directamente saltándose Angular. A eso se suma la Content Security Policy como segunda línea de defensa. Un detalle importante que se olvida: la protección de Angular cubre las plantillas, no las URL que construyes a mano ni lo que hagas con librerías de terceros.
Medio ¿Pipe puro o impuro?
Un pipe puro solo se recalcula cuando cambia la referencia de su entrada, y es el comportamiento por defecto. Uno impuro se ejecuta en cada ciclo de detección de cambios, lo que en una lista de doscientos elementos significa doscientas ejecuciones por ciclo. Los impuros existen para casos como filtrar por una propiedad mutable, pero casi siempre hay una alternativa mejor: calcular el valor derivado en el componente con un computed, o hacer inmutables los datos. En una entrevista, mencionar el coste del pipe impuro en la detección de cambios demuestra que entiendes lo que pasa por debajo.
Junior ¿Qué diferencia hay entre una directiva de atributo y una estructural?
La de atributo cambia la apariencia o el comportamiento de un elemento que ya existe (resaltar, añadir una escucha, gestionar el foco). La estructural añade o quita elementos del DOM, y por eso trabaja con TemplateRef y ViewContainerRef. En Angular moderno los casos más comunes de directiva estructural (condicionales y bucles) los cubre el control de flujo nativo @if/@for, que es más rápido y no necesita importar nada, pero seguir sabiendo escribir una directiva estructural propia sigue siendo útil para casos como «renderiza esto solo si el usuario tiene tal permiso».
Senior ¿Qué es ViewEncapsulation y qué implica cada modo?
Emulated, el modo por defecto, simula el aislamiento añadiendo un atributo único a los elementos del componente y reescribiendo los selectores del CSS: los estilos no se escapan, pero sí pueden entrar los globales. None desactiva el aislamiento y los estilos se aplican a toda la aplicación, lo que es útil para estilos globales deliberados y peligroso por accidente. ShadowDom usa el Shadow DOM nativo, con aislamiento real en ambos sentidos, a costa de que los estilos globales y muchas librerías de terceros dejen de alcanzar el interior. En la práctica se usa Emulated casi siempre, con variables CSS para permitir la personalización controlada desde fuera.
Senior ¿Cómo migrarías una aplicación grande de NgModules a standalone?
Sin big bang. Angular ofrece un schematic de migración automática que hace la mayor parte del trabajo mecánico, y ambos mundos interoperan: un componente standalone puede importar un módulo y un módulo puede declarar componentes standalone en sus imports. La estrategia sensata es migrar por feature, empezando por las hojas del árbol (componentes de presentación sin dependencias), subir después a los contenedores, y dejar el arranque para el final, cuando ya casi todo es standalone. Cada paso debe quedar desplegable y con los tests en verde; una migración de meses en una rama larga es la forma más segura de que nunca se termine.
Medio ¿Qué es NgOptimizedImage?
Una directiva que aplica automáticamente las buenas prácticas de carga de imágenes: exige las dimensiones para reservar el espacio y evitar saltos de diseño, genera el atributo srcset para servir el tamaño adecuado a cada pantalla, aplica carga perezosa por defecto y permite marcar la imagen principal como prioritaria para que se precargue. Afecta directamente a dos métricas de experiencia de usuario que Google mide: el mayor elemento visible y el desplazamiento acumulado del diseño. Es de esas cosas que dan una mejora medible por muy poco trabajo.
Senior ¿Qué son las Core Web Vitals y cómo las mejorarías en Angular?
Son las métricas con las que se mide la experiencia real: el tiempo hasta que se pinta el mayor elemento visible, la estabilidad visual (que no bailen los elementos) y la capacidad de respuesta a la interacción. En Angular las palancas concretas son: renderizado en servidor o prerenderizado para adelantar el primer pintado; NgOptimizedImage y dimensiones explícitas para la estabilidad; carga diferida de rutas y @defer para reducir el JavaScript inicial, que es lo que más afecta a la capacidad de respuesta; OnPush o el modo sin zona para que la interacción no dispare trabajo innecesario; y fuentes con font-display adecuado. Y medir en campo, no solo en el laboratorio.
Medio ¿Cómo pasarías datos entre componentes que no tienen relación padre-hijo?
Por orden de preferencia: un servicio compartido con señales, que es lo más simple y directo; parámetros de la URL, si el estado debería sobrevivir a una recarga o poder compartirse mediante enlace (un filtro, una pestaña activa); un store si el estado es complejo y lo consumen muchas vistas; y el enrutador con withComponentInputBinding, que enlaza automáticamente los parámetros de ruta a las entradas del componente. Lo que hay que evitar es la cadena de entradas y salidas atravesando cinco niveles de componentes que no usan el dato para nada, porque acopla toda la jerarquía.
Junior ¿Qué hace el CLI y qué comandos usas a diario?
ng serve para el servidor de desarrollo con recarga automática, ng generate para crear componentes, servicios, guards e interceptores con la estructura y las convenciones correctas, ng build para la compilación de producción, ng test y ng lint para calidad, y ng update, que merece mención aparte: no solo actualiza las dependencias, sino que ejecuta migraciones automáticas que reescriben tu código adaptándolo a los cambios de la nueva versión. Es la razón por la que actualizar Angular es mucho menos doloroso que en otros ecosistemas.
Senior ¿Qué es un ControlValueAccessor?
Es el puente que permite que un componente propio se comporte como un control nativo dentro de un formulario de Angular, respondiendo a formControlName y a ngModel. Hay que implementar cuatro métodos: writeValue (el formulario escribe en tu componente), registerOnChange (tu componente avisa de que el usuario cambió el valor), registerOnTouched (avisar al perder el foco, que es lo que casi todo el mundo olvida y por eso sus validaciones no se muestran cuando deberían) y setDisabledState. Se registra con el proveedor NG_VALUE_ACCESSOR y multi: true. Es la forma correcta de encapsular un selector de etiquetas, un editor enriquecido o un campo de fecha propio.
Senior ¿Qué diferencia hay entre resolver los datos con un ResolveFn o cargarlos en el componente?
Con un resolver, la navegación no se completa hasta que los datos están listos: el usuario permanece en la pantalla anterior y aterriza en una vista ya poblada, sin estados vacíos intermedios. El inconveniente es que la aplicación parece congelada si la carga tarda, así que conviene combinarlo con un indicador de progreso global y no meter ahí peticiones lentas. Cargar en el componente permite mostrar la estructura de la página al instante con esqueletos de carga, que hoy suele considerarse mejor experiencia. La respuesta madura: resolver para los datos imprescindibles y pequeños (el proyecto que da nombre a la pantalla) y carga en el componente con esqueletos para las listas.

23.4 NestJS

Junior ¿Qué aporta NestJS frente a Express puro?
Express te da un enrutador y poco más: cada equipo inventa su propia estructura, su forma de inyectar dependencias y su manejo de errores, y al cambiar de proyecto hay que aprenderlo todo otra vez. NestJS aporta una arquitectura definida (módulos, controladores, proveedores), inyección de dependencias con contenedor, un ciclo de petición con puntos de extensión claros, integración con TypeScript y decoradores, y un ecosistema oficial para configuración, validación, documentación, colas, WebSockets y microservicios. El coste es la curva de aprendizaje y una capa de abstracción sobre Express o Fastify. La comparación honesta: en un servicio de tres endpoints, Nest es exceso de ceremonia; en una API que va a mantener un equipo durante años, es una inversión que se recupera pronto.
Junior ¿Qué es un módulo y cómo se organizan?
Una clase con @Module que agrupa un trozo coherente de funcionalidad y declara cuatro cosas: imports (otros módulos cuyos exportados necesita), controllers, providers (lo que se puede inyectar dentro) y exports (lo que ofrece a quien lo importe). La regla clave, y motivo de muchos NullInjectorError: un proveedor solo es visible fuera si se exporta explícitamente. La organización habitual es un módulo por dominio o feature, más un módulo compartido para utilidades transversales, evitando el «módulo común» que acaba conteniendo todo y del que depende media aplicación.
Medio ¿Qué es un módulo dinámico y para qué se usa?
Un módulo que se configura al importarlo, mediante métodos estáticos que devuelven la definición del módulo. La convención es forRoot() para la configuración global que se hace una sola vez en el módulo raíz (conexión a la base de datos, configuración global), forFeature() para el registro parcial en cada módulo que lo necesita (los repositorios de ciertas entidades), y el sufijo Async cuando la configuración depende de otro servicio, típicamente para leer variables de entorno con useFactory e inject. Es el patrón que usan ConfigModule, JwtModule, MikroOrmModule y prácticamente todos los módulos del ecosistema.
Senior Explica el orden exacto del ciclo de una petición en NestJS.
Es la pregunta estrella. El orden es: middlewareguardsinterceptores (parte anterior)pipesmanejador del controladorinterceptores (parte posterior)filtros de excepción si algo lanzó → respuesta. Los detalles que demuestran que lo has vivido: los pipes se ejecutan después de los guards, así que un guard no puede contar con el cuerpo ya validado y transformado; los interceptores envuelven al manejador como una cebolla, de ahí que puedan medir tiempos y transformar la respuesta; y los filtros capturan también lo que lancen los guards y los interceptores. Dentro de cada tipo, el orden es global, luego de controlador, luego de método.
Medio ¿Middleware, guard o interceptor? ¿Cómo eliges?
Middleware para lo que es puramente de la capa HTTP y no necesita conocer el manejador que se va a ejecutar: cabeceras de seguridad, identificador de correlación, cuerpo sin procesar para un webhook, el contexto de petición del ORM. Guard cuando la respuesta es «sí o no puede pasar»: autenticación, autorización, comprobación de propiedad del recurso; tiene acceso a los metadatos del handler mediante Reflector, que es justo lo que el middleware no tiene. Interceptor cuando quieres envolver la ejecución: medir tiempos, transformar la respuesta, cachear, aplicar un timeout, gestionar una transacción por petición. Si dudas entre guard e interceptor, pregúntate si vas a bloquear (guard) o envolver (interceptor).
Medio ¿Cómo funciona Reflector y para qué sirve?
Permite leer en tiempo de ejecución los metadatos que un decorador dejó en el manejador o en la clase. Es el mecanismo que hace posible el patrón de guard de roles: @Roles('admin') guarda la lista, y el guard la recupera con reflector.getAllAndOverride(ROLES_KEY, [handler, clase]), que da prioridad al método sobre el controlador. getAllAndMerge los combina en lugar de sobrescribir, útil para acumular permisos. En versiones recientes existe Reflector.createDecorator, que crea decoradores tipados y evita las claves mágicas en forma de cadena.
Medio ¿Qué hace el ValidationPipe y cómo lo configuras?
Toma el cuerpo, los parámetros o la query, los convierte en una instancia del DTO y ejecuta las reglas de class-validator, lanzando un 400 con los errores si algo falla. La configuración que debería estar en cualquier proyecto: whitelist: true para eliminar las propiedades no declaradas en el DTO, forbidNonWhitelisted: true para rechazar directamente si llegan campos desconocidos, y transform: true para obtener instancias reales de la clase y no objetos planos. Sobre enableImplicitConversion conviene ser prudente: resuelve el problema de que los parámetros de la URL llegan siempre como cadena, pero convierte de forma agresiva y puede producir sorpresas; muchos equipos prefieren @Type(() => Number) explícito.
Medio ¿Por qué no usar las entidades como DTO?
Por cuatro motivos que conviene enumerar en la entrevista. Seguridad: expones campos que no debes (el hash de la contraseña, marcas internas) y abres la puerta a la asignación masiva, donde un cliente manda role: 'admin' y se lo asignas. Acoplamiento: tu API pasa a ser un reflejo de tu esquema, y cualquier cambio en la base de datos rompe a los clientes. Validación: las reglas de entrada no son las mismas que las invariantes de persistencia. Y claridad: el DTO de creación y el de actualización son distintos, y el de salida también. La contrapartida es más código y mapeo, que es exactamente lo que se paga a cambio.
Medio ¿Qué son los ámbitos de un proveedor?
Por defecto, DEFAULT: una única instancia compartida en toda la aplicación, creada al arrancar. REQUEST crea una instancia por petición, lo que permite inyectar el objeto de la petición pero propaga el ámbito hacia arriba: todo lo que dependa de ese proveedor se vuelve también por petición, con el coste de rendimiento correspondiente. TRANSIENT da una instancia nueva a cada consumidor. La respuesta senior: evita REQUEST siempre que puedas y resuelve la necesidad con AsyncLocalStorage o con el RequestContext del ORM, porque un proveedor por petición en la raíz del grafo puede degradar seriamente el rendimiento.
Medio ¿Cómo resuelves una dependencia circular?
Nest ofrece forwardRef() tanto para módulos como para servicios, y funciona. Pero antes de usarlo conviene reconocer que una dependencia circular casi siempre es un síntoma de diseño: si A necesita a B y B necesita a A, probablemente hay un tercer concepto que ambos comparten y que debería extraerse a su propio módulo, o la comunicación debería invertirse mediante eventos con el EventEmitter. Decir esto en una entrevista, y solo después mencionar forwardRef como salida táctica, marca la diferencia entre conocer la API y tener criterio.
Medio ¿Cómo estructuras el manejo de errores?
Con tres capas. El dominio lanza excepciones propias que no saben nada de HTTP (TaskNotFoundError, ProjectArchivedError): eso mantiene la lógica de negocio reutilizable desde una cola o un comando de consola. Un filtro de excepción global las traduce a códigos y cuerpos HTTP coherentes, idealmente en formato Problem Details, añadiendo un identificador de correlación. Y ese mismo filtro registra los 500 con traza completa mientras devuelve al cliente un mensaje genérico, para no filtrar detalles internos. Un detalle que gusta oír: mapear los errores del ORM (violación de restricción única, clave foránea) a códigos correctos, en lugar de dejar que todo acabe en un 500.
Medio ¿Cómo implementas autenticación con JWT?
El usuario envía credenciales; se verifica el hash de la contraseña con bcrypt o argon2; se emiten dos tokens: uno de acceso de vida corta (minutos) y uno de refresco de vida larga. El de acceso viaja en cada petición y lo valida un guard global, con las rutas públicas marcadas mediante un decorador @Public(). El de refresco permite obtener uno nuevo sin volver a pedir credenciales, y debe rotarse en cada uso y almacenarse (o su hash) para poder revocarlo. Los puntos que se esperan de un perfil medio: firmar con un algoritmo asimétrico o un secreto fuerte, validar siempre emisor, audiencia y caducidad, y no meter datos sensibles en el token porque su contenido es legible por cualquiera, solo está firmado.
Senior ¿Dónde guardas el token en el cliente?
Es una pregunta trampa clásica y la respuesta correcta empieza por «depende del modelo de amenaza». En localStorage es cómodo y funciona entre dominios, pero cualquier XSS puede leerlo y exfiltrarlo. En una cookie HttpOnly, Secure y SameSite, el JavaScript no puede leerla, lo que neutraliza esa vía, pero introduce el riesgo de CSRF, que hay que mitigar con SameSite=Lax o Strict y, si hace falta, un token anti-CSRF. La recomendación habitual hoy es la cookie para el token de refresco y mantener el de acceso en memoria. Y el matiz que cierra la respuesta: si tienes un XSS, has perdido igualmente, porque el atacante puede hacer peticiones en nombre del usuario aunque no vea el token.
Medio ¿bcrypt o argon2?
Los dos son correctos y ambos son deliberadamente lentos, que es justo lo que se busca para frenar los ataques por fuerza bruta. Argon2id es el más recomendado hoy porque además resiste mejor los ataques con hardware especializado gracias a su coste en memoria, y es el ganador de la competición de hashing de contraseñas. Bcrypt lleva décadas en producción, está muy probado y tiene la peculiaridad de truncar la entrada a 72 bytes, algo que hay que conocer. Lo que nunca se debe responder: SHA-256 o MD5, que son rápidos por diseño y por tanto pésimos para contraseñas.
Medio ¿Cómo implementas autorización basada en roles?
Un decorador @Roles('admin', 'owner') que guarda metadatos, y un guard global que los lee con Reflector y los compara con los roles del usuario autenticado. En cuanto el sistema crece, los roles simples se quedan cortos y hay que pasar a permisos («puede editar tareas del proyecto X»), lo que suele implicar comprobar la propiedad del recurso consultando la base de datos, no solo el token. Para casos complejos existen bibliotecas de políticas tipo CASL. El error frecuente: hacer toda la comprobación en el guard cuando depende de datos que el servicio ya va a cargar, duplicando la consulta.
Medio ¿Cómo cachearías respuestas en Nest?
Con el módulo de caché, respaldado por memoria para un solo proceso o por Redis en cuanto haya varias instancias, porque una caché en memoria por réplica produce respuestas incoherentes según a quién te toque. Se puede aplicar de forma declarativa con un interceptor sobre rutas GET o de forma manual en el servicio, que da más control. Los tres puntos difíciles, y lo que realmente se pregunta: la clave debe incluir todo lo que diferencia la respuesta (incluido el usuario, o filtrarás datos ajenos), la invalidación al escribir, y decidir un tiempo de vida honesto. También conviene distinguir esta caché de la caché HTTP con ETag, que evita transferir datos pero no evita el trabajo del servidor.
Medio ¿Cuándo usarías una cola de trabajos?
Siempre que una operación no tenga que completarse dentro de la petición HTTP: enviar correos, generar informes o PDF, procesar imágenes, sincronizar con sistemas externos, o cualquier cosa que pueda fallar y merezca reintentos. El patrón es responder 202 Accepted con un identificador de trabajo y procesar en segundo plano con BullMQ sobre Redis, que aporta reintentos con retroceso exponencial, prioridades, trabajos programados y una cola de fallidos. Los dos requisitos que hay que mencionar: los trabajos deben ser idempotentes, porque se pueden ejecutar dos veces, y hay que vigilar la cola de fallidos, porque una cola que nadie mira es una forma silenciosa de perder datos.
Medio ¿Cómo proteges la API frente a abusos?
Limitación de peticiones por IP y por usuario, con límites más estrictos en los endpoints sensibles como el login, donde además conviene un retardo progresivo. A eso se suma: limitar el tamaño del cuerpo, validar estrictamente con lista blanca, paginación obligatoria con un máximo real (si aceptas limit=100000, alguien lo usará), timeouts en las llamadas salientes, cabeceras de seguridad con helmet, CORS restringido a los orígenes conocidos, y un cortafuegos de aplicación o CDN delante. Y registrar lo suficiente para detectar el abuso: sin observabilidad no sabrás que te están atacando hasta que se caiga el servicio.
Senior ¿Cuándo usarías WebSockets y qué complica?
Cuando el servidor necesita empujar datos al cliente con baja latencia: chat, notificaciones, colaboración en tiempo real, paneles en vivo. Lo que complica es todo lo que da por sentado el modelo petición-respuesta: la autenticación en el handshake y su renovación, la reconexión con recuperación del estado perdido, y sobre todo el escalado horizontal, porque una conexión vive en una instancia concreta y para emitir a todos los usuarios hace falta un adaptador con Redis que distribuya los mensajes entre instancias. Antes de asumir eso conviene preguntarse si un sondeo cada treinta segundos o un flujo de eventos del servidor bastarían, que es una respuesta que suele gustar.
Senior ¿REST o GraphQL?
REST encaja cuando los recursos están bien definidos, los clientes son homogéneos y quieres aprovechar la caché HTTP, los códigos de estado y la simplicidad operativa. GraphQL brilla cuando hay muchos clientes distintos con necesidades de datos distintas, cuando el problema real es el exceso o defecto de datos en cada respuesta, o cuando quieres un grafo unificado sobre varios servicios. Los costes de GraphQL que hay que nombrar: la caché deja de ser gratis, el problema N+1 se vuelve estructural y obliga a usar DataLoader, hay que limitar la profundidad y complejidad de las consultas para que nadie tumbe el servidor con una consulta anidada, y la observabilidad es más difícil porque todo pasa por un único endpoint.
Senior ¿Cuándo pasarías a microservicios?
Más tarde de lo que la gente cree. Los microservicios resuelven problemas organizativos y de escalado independiente: equipos que se pisan al desplegar, componentes con requisitos de recursos muy distintos, necesidad de tecnologías diferentes. A cambio traen latencia de red, consistencia eventual, transacciones distribuidas, despliegues coordinados, y una complejidad operativa que exige observabilidad seria. Para casi todos los proyectos, la respuesta correcta es un monolito modular bien diseñado, con fronteras claras entre módulos, que permite extraer un servicio el día que de verdad haga falta. Nest facilita ambos caminos, y su sistema de transportes permite convertir un módulo en microservicio sin reescribir la lógica.
Medio ¿Cómo versionas una API?
Nest soporta versionado por URI (/v1/tasks), por cabecera, por tipo de medio o personalizado. Por URI es lo más común y lo más fácil de depurar y cachear. Lo interesante de la pregunta no es la sintaxis sino la estrategia: versionar toda la API a la vez genera versiones enormes que nadie migra, y lo razonable es evitar los cambios rompedores siempre que se pueda (añadir campos no rompe; quitarlos o renombrarlos sí), avisar con deprecación y métricas de uso antes de retirar nada, y mantener como mucho dos versiones vivas con una fecha de fin anunciada.
Medio ¿Cómo gestionas la configuración y los secretos?
Con el módulo de configuración, cargando variables de entorno y validando el esquema al arrancar, de modo que si falta una variable la aplicación falle inmediatamente y no tres horas después en la primera petición que la necesite. Los secretos nunca en el repositorio: en desarrollo, un .env ignorado por Git con un .env.example documentado; en producción, el gestor de secretos de la plataforma. Y conviene tipar la configuración y exponerla mediante un servicio, en lugar de esparcir process.env por todo el código, que es lo que hace imposible saber qué variables necesita realmente el sistema.
Medio ¿Qué es Swagger/OpenAPI y qué te aporta?
Una descripción formal de tu API que se genera a partir de los decoradores y de los DTO. Te da documentación interactiva siempre sincronizada con el código, generación de clientes tipados para el frontend, y una base para pruebas de contrato. El consejo práctico: activa el plugin del CLI para que infiera gran parte de los metadatos sin escribir cincuenta decoradores, documenta los códigos de error además de los casos correctos, y protege o desactiva la interfaz en producción si la API no es pública.
Senior ¿Express o Fastify como adaptador?
Express es el predeterminado, tiene el mayor ecosistema de middleware y es lo que casi todo el mundo conoce. Fastify ofrece más rendimiento (serialización JSON basada en esquemas y un enrutador más rápido) y validación integrada, a cambio de que algunos middleware de Express no sean compatibles directamente. La respuesta con criterio: el adaptador rara vez es el cuello de botella real, que suele estar en la base de datos; cambia a Fastify si has medido que la sobrecarga del framework importa en tu caso, no por el número de un benchmark sintético.
Medio ¿Cómo testeas en NestJS?
Tres niveles. Unitario: instancias directas del servicio con dobles de sus dependencias, sin tocar el contenedor; rapidísimos, ideales para la lógica de negocio. Integración: Test.createTestingModule con el módulo real y una base de datos de verdad (un contenedor efímero), que es donde se detectan los problemas de consultas, transacciones y mapeo. Extremo a extremo: Supertest contra la aplicación completa levantada, para los recorridos críticos y la seguridad. El error habitual es sustituir el repositorio con un doble en los tests de repositorio, con lo que acabas comprobando tu doble y no tu SQL.
Senior ¿Qué es CQRS y cuándo aporta?
Separar el modelo de escritura del de lectura: los comandos modifican estado y no devuelven datos, las consultas leen y no modifican nada. Aporta cuando las necesidades de ambos lados divergen de verdad: escrituras con reglas de negocio complejas frente a lecturas que necesitan agregaciones rápidas desde vistas materializadas o un almacén distinto. En un CRUD normal es complejidad gratuita y duplicación de código. Nest tiene un módulo para ello con buses de comandos, consultas y eventos, útil también sin llegar a separar los almacenes.
Medio ¿Cómo implementas la salud del servicio y qué expones?
Con Terminus se exponen endpoints de salud que comprueban dependencias reales: base de datos, Redis, servicios externos, memoria y disco. Lo importante es distinguir dos sondas, porque los orquestadores las usan de forma distinta: la de liveness responde «el proceso está vivo, no lo reinicies» y no debe depender de servicios externos, mientras que la de readiness responde «estoy listo para recibir tráfico» y sí debe comprobar la base de datos. Confundirlas provoca reinicios en cascada cuando la base de datos tiene un hipo pasajero.

23.5 MikroORM y bases de datos

Medio ¿Data Mapper o Active Record?
En Active Record la entidad sabe persistirse: usuario.save(). Es directo y cómodo para casos simples, pero mezcla el modelo de dominio con la persistencia y complica los tests. MikroORM usa Data Mapper: las entidades son objetos de dominio sin dependencia del acceso a datos, y un EntityManager aparte se encarga de persistirlas. Eso mantiene el dominio limpio, encaja con la inyección de dependencias y permite el Unit of Work. El precio es más ceremonia: hay que pasar el EntityManager por ahí y entender su ciclo de vida.
Senior Explica Unit of Work e Identity Map.
El Identity Map garantiza que, dentro de un contexto, cada fila de la base de datos esté representada por un único objeto en memoria: si pides dos veces la tarea 5, recibes exactamente la misma instancia. Eso evita incoherencias y ahorra consultas. El Unit of Work registra los cambios que haces sobre esos objetos y no toca la base de datos hasta el flush(), momento en que calcula el conjunto mínimo de operaciones, las ordena respetando las dependencias entre entidades y las ejecuta dentro de una transacción. La consecuencia práctica que hay que saber explicar: no necesitas llamar a un update, basta con modificar el objeto y hacer flush; y por eso mismo un flush puede lanzar consultas que no escribiste tú.
Senior ¿Por qué es peligroso un EntityManager global?
Porque su Identity Map es estado compartido. Si dos peticiones concurrentes usan el mismo gestor, ven y modifican las mismas instancias, y un flush() de una puede persistir los cambios a medias de la otra. Es una fuga de datos entre usuarios esperando a ocurrir. La solución en Nest es RequestContext, un middleware que crea un fork() del gestor por petición, aislando el contexto. Para trabajos en segundo plano y tareas programadas, donde no hay petición, hay que crear el fork explícitamente. Esta es una de las preguntas que mejor distingue a quien ha llevado MikroORM a producción.
Medio ¿Qué es el problema N+1 y cómo lo detectas?
Ocurre cuando cargas N entidades y luego, al acceder a una relación de cada una, se lanza una consulta por cada elemento: 1 + N consultas donde debería haber 1 o 2. Es la causa más común de que una API vaya bien en desarrollo con diez filas y se arrastre en producción con diez mil. Se detecta activando el registro de SQL y mirándolo (lo más eficaz), o con trazas distribuidas donde aparece un abanico de consultas idénticas. Se soluciona con populate, con una estrategia de carga adecuada o con un QueryBuilder que haga el join. El matiz senior: la carga ansiosa indiscriminada tampoco es la solución, porque traer un grafo enorme para mostrar tres campos también es un problema.
Medio ¿Qué es Reference<T> y por qué existe?
Es un envoltorio que representa una relación que puede no estar cargada todavía. Existe porque, sin él, no sabrías si tarea.proyecto.nombre va a devolver el nombre o va a fallar por estar la relación sin inicializar. Con Reference, el sistema de tipos te obliga a ser explícito: tarea.proyecto.id siempre está disponible (es la clave foránea, que ya tienes), pero para el resto necesitas cargarla con load() o haberla incluido en populate. Convierte un error de ejecución en un error de compilación, que es exactamente lo que quieres.
Medio ¿Qué hace em.getReference()?
Crea una entidad «fantasma» con solo la clave primaria, sin consultar la base de datos. Es la forma correcta de asignar una relación cuando ya conoces el identificador: para crear una tarea en el proyecto 7 no necesitas cargar el proyecto entero, basta con la referencia, y ahorras una consulta. Al hacer flush, el ORM escribe la clave foránea correcta. La precaución: si accedes a cualquier propiedad que no sea la clave, tendrás que cargarla, y si el identificador no existe, el error llegará al insertar por violación de clave foránea.
Medio ¿Cuándo usarías QueryBuilder en vez del EntityManager?
El EntityManager con find y filtros cubre la inmensa mayoría de los casos y devuelve entidades gestionadas por el Unit of Work. El QueryBuilder es la salida cuando necesitas SQL que no se expresa bien con objetos: agregaciones complejas, subconsultas, funciones de ventana, CTE, joins con condiciones raras, o actualizaciones masivas. El precio a recordar: los resultados crudos no pasan por el Identity Map ni por los filtros globales, así que un borrado lógico implementado con un filtro no se aplica automáticamente a tu consulta del QueryBuilder. Eso ha causado más de un incidente.
Senior ¿Bloqueo optimista o pesimista?
Optimista: se asume que los conflictos son raros; se añade una columna de versión y el UPDATE incluye WHERE version = ?; si no afecta a ninguna fila, alguien te adelantó y devuelves un 409 para que el cliente reintente o resuelva. No bloquea nada y escala bien, así que es la opción por defecto en una API web. Pesimista: se bloquea la fila con SELECT ... FOR UPDATE desde el principio; los demás esperan. Es lo correcto cuando el conflicto es probable y reintentar es caro o imposible, como al reservar el último asiento o mover dinero. El riesgo del pesimista son las esperas y los interbloqueos si no bloqueas siempre en el mismo orden.
Medio ¿Cómo gestionas las migraciones en producción?
Migraciones versionadas en el repositorio, revisadas como cualquier otro código, y nunca sincronización automática del esquema en producción. Se ejecutan como un paso separado del despliegue, no al arrancar la aplicación (si arrancan diez réplicas a la vez, diez procesos intentan migrar). Y las que importan de verdad son las compatibles hacia atrás, con el patrón expandir y contraer: primero añades la columna nueva y escribes en las dos, despliegas el código que la usa, migras los datos, y solo en un despliegue posterior eliminas la antigua. Así en ningún momento hay una versión del código incompatible con el esquema, que es lo que permite desplegar sin caída y poder revertir.
Medio ¿Cómo implementas el borrado lógico?
Con una columna deleted_at y un filtro global del ORM que añada deleted_at IS NULL a todas las consultas de esa entidad, de modo que el resto del código no tenga que acordarse. Los tres detalles que se suelen olvidar y que conviene mencionar: las restricciones únicas deben pasar a ser índices únicos parciales, o no podrás reutilizar el email de un usuario borrado; el filtro no se aplica al SQL nativo ni siempre al QueryBuilder; y hay que decidir qué pasa con los hijos, que normalmente deben ocultarse también. Y recordar que el borrado lógico no cumple por sí solo el derecho al olvido: eso exige borrado o anonimización real.
Senior ¿Qué diferencia hay entre flush y una transacción?
Un flush() envía los cambios pendientes a la base de datos, envueltos en su propia transacción implícita si no hay ninguna abierta. Una transacción explícita agrupa varias operaciones (que pueden incluir varios flush) en una unidad atómica. La diferencia importa cuando un caso de uso hace dos escrituras relacionadas: con dos flush sueltos, la primera puede confirmarse y la segunda fallar, dejando datos inconsistentes. Por eso los casos de uso que modifican varias entidades deben ir dentro de em.transactional(). Y dentro de una transacción hay que evitar llamadas de red o a servicios externos, porque mantienen la transacción abierta y con ella los bloqueos.
Medio ¿Qué es la paginación por cursor y cuándo la prefieres?
En lugar de OFFSET, se pide «lo que viene después de este valor», usando la columna de orden más un desempate único. Se prefiere en dos situaciones: con desplazamientos grandes, porque OFFSET 100000 obliga a la base de datos a generar y descartar cien mil filas, mientras que el cursor va directo por el índice; y con datos que cambian mientras se navega, porque con offset una inserción provoca que veas dos veces la misma fila o que te saltes otra. Su limitación es que no permite saltar a la página 47, así que encaja con listas de desplazamiento infinito y no con tablas que necesitan navegación numerada.
Medio ¿Cuándo se usa un índice y cuándo no?
Un B-tree sirve para igualdad, rangos, ordenación y prefijos de texto. Deja de servir si aplicas una función sobre la columna (lower(email) sin un índice de expresión), si el comodín está al principio (LIKE '%x'), si comparas tipos distintos obligando a una conversión, o si el filtro es tan poco selectivo que leer la tabla entera sale más barato, que es una decisión legítima del planificador. En un índice compuesto se aprovecha por el prefijo izquierdo: (a, b, c) sirve para filtrar por a, por a y b, o por los tres, pero no para filtrar solo por b.
Medio ¿EXISTS, IN o JOIN?
EXISTS cuando solo quieres filtrar: no duplica filas y el motor puede parar en la primera coincidencia. JOIN cuando necesitas columnas de la otra tabla; si al escribirlo te sale un DISTINCT defensivo, casi seguro que querías un EXISTS. IN con una lista literal corta está bien, pero NOT IN con una subconsulta es una trampa: si la subconsulta devuelve algún NULL, el resultado es vacío por la lógica de tres valores. Usa NOT EXISTS.
Medio ¿Qué tipo usarías para dinero y para fechas?
Para dinero, numeric con escala fija o enteros de céntimos; jamás coma flotante, porque 0.1 + 0.2 no es 0.3 en binario y las facturas descuadran. Para instantes, timestamptz, almacenando siempre en UTC y convirtiendo en la presentación; el timestamp sin zona es una fuente inagotable de errores en cuanto hay un cambio de hora o un usuario en otro país. Para fechas sin hora, como un cumpleaños o un vencimiento, date, porque no tienen zona horaria. Y para identificadores, bigint desde el principio o UUID versión 7 si necesitas que sean opacos.
Senior ¿Cómo diagnosticas una consulta lenta?
Con EXPLAIN (ANALYZE, BUFFERS), que ejecuta la consulta y muestra el plan real con tiempos y páginas leídas. Se lee de dentro hacia fuera buscando el nodo que consume el tiempo. Las señales típicas: un Seq Scan sobre una tabla grande con muchas filas descartadas por el filtro (falta un índice), una gran diferencia entre filas estimadas y reales (estadísticas obsoletas: hace falta ANALYZE), una ordenación que se va a disco (falta memoria o un índice que sirva ese orden), o muchas lecturas físicas frente a lecturas de caché. Y siempre: medir antes y después para demostrar la mejora.
Medio ¿Qué es la normalización y hasta dónde llegarías?
Es organizar el esquema para que cada hecho se almacene una sola vez, evitando que dos copias puedan contradecirse. En la práctica se llega a la tercera forma normal: valores atómicos, sin dependencias parciales de una clave compuesta y sin dependencias entre atributos no clave. A partir de ahí se desnormaliza de forma deliberada y medida: contadores agregados para evitar un COUNT caro, campos calculados muy consultados, o copias históricas como el precio de una línea de factura, que no es redundancia sino un hecho del pasado que no debe cambiar. La condición innegociable es que exista un mecanismo que mantenga la copia sincronizada.
Senior ¿Qué niveles de aislamiento conoces y cuál usas?
De menos a más estricto: lectura no confirmada, lectura confirmada (el predeterminado en PostgreSQL), lectura repetible y serializable. Con lectura confirmada cada instrucción ve una instantánea nueva, lo que permite lecturas no repetibles y, sobre todo, la actualización perdida: dos transacciones leen el mismo valor, ambas escriben y una pisa a la otra sin que nadie reciba un error. Se resuelve con una actualización atómica (SET saldo = saldo - 10), con bloqueo pesimista o con una columna de versión. Serializable da la garantía completa a cambio de que algunas transacciones aborten y tu código tenga que reintentarlas.
Medio ¿Qué son los embeddables y cuándo los usas?
Objetos de valor que se almacenan dentro de la tabla de la entidad, sin tabla propia: una dirección, un rango de fechas, un importe con su moneda. Aportan cohesión al modelo (la lógica de validar un código postal vive en la clase Direccion, no suelta en la entidad) sin el coste de un join. Es la aplicación directa del concepto de objeto de valor de DDD: no tienen identidad propia, se comparan por valor y son inmutables. Si necesitas consultarlos de forma independiente o compartirlos entre entidades, entonces sí toca una tabla.
Medio ¿Cuándo usarías jsonb?
Para datos genuinamente variables cuya forma no controlas o que cambia por cliente: la carga útil de un webhook, preferencias de usuario, campos de un formulario definido por el usuario. Se puede indexar con GIN y consultar con operadores de contención. Cuándo es un error: usarlo para no decidir el esquema. Dentro de un jsonb no hay claves foráneas, ni tipos, ni NOT NULL, las consultas son más lentas y cada cambio de forma se convierte en un script de transformación. La regla: si un campo se consulta o se filtra a menudo, merece ser una columna.

23.6 Arquitectura, diseño y buenas prácticas

Medio Explica SOLID con un ejemplo de cada principio.
Responsabilidad única: un servicio que crea tareas y además envía correos tiene dos motivos para cambiar; separa la notificación. Abierto/cerrado: en lugar de un switch por tipo de notificación que hay que tocar cada vez, un conjunto de estrategias registradas con multi: true. Sustitución de Liskov: si ReadOnlyRepository hereda de Repository y su método save lanza una excepción, has roto el contrato: quien use la clase base no puede sustituirla sin romperse. Segregación de interfaces: mejor Lector y Escritor por separado que una interfaz de veinte métodos que obliga a implementar quince vacíos. Inversión de dependencias: el servicio depende de un token abstracto y la configuración decide la implementación concreta, que es exactamente lo que hace la inyección de dependencias de Angular y Nest.
Medio ¿Qué es la arquitectura en capas y qué reglas tiene?
Presentación (controladores), aplicación (casos de uso), dominio (entidades y reglas) e infraestructura (base de datos, servicios externos). La regla que le da sentido es que las dependencias apuntan hacia dentro: el dominio no conoce a nadie, y desde luego no importa nada del ORM ni de HTTP. El beneficio práctico es que puedes probar las reglas de negocio sin base de datos y cambiar de framework sin reescribir el dominio. El riesgo es el exceso de ceremonia: en un CRUD sencillo, cuatro capas y tres mapeos para guardar un nombre es puro coste. La madurez consiste en saber cuándo aplicarlo y cuándo no.
Senior ¿Qué es la arquitectura hexagonal?
Puertos y adaptadores: el núcleo de la aplicación define interfaces (puertos) para todo lo que necesita del exterior, y los adaptadores las implementan. El puerto de repositorio lo implementa un adaptador con MikroORM; el puerto de notificación, uno con correo electrónico; el puerto de entrada lo usa un adaptador HTTP, pero también podría usarlo uno de línea de comandos o un consumidor de cola. La ventaja real no es «poder cambiar de base de datos», que casi nunca ocurre, sino la testabilidad: pruebas el núcleo con adaptadores en memoria, rápidos y deterministas. El coste es más indirección, y en un proyecto pequeño puede no compensar.
Senior ¿Qué te llevas de DDD a un proyecto normal?
Sin necesidad de adoptarlo entero, cuatro ideas rinden desde el primer día. El lenguaje ubicuo: que el código use las mismas palabras que el negocio, sin traducciones mentales. Los objetos de valor: Email, Dinero o RangoFechas como clases con validación propia en lugar de cadenas y números sueltos. Los agregados: definir qué entidades se modifican juntas y por dónde se entra a modificarlas, lo que da fronteras naturales a las transacciones. Y los contextos delimitados: aceptar que «usuario» significa cosas distintas en facturación y en soporte, en vez de construir un modelo único que no sirve a nadie.
Medio ¿Qué patrones de diseño usas de verdad en este stack?
Más de los que parece, aunque los frameworks los oculten. Inyección de dependencias y singleton en los proveedores. Repositorio en la capa de datos. Decorador en los interceptores de Nest, que envuelven la ejecución. Cadena de responsabilidad en el ciclo de petición y en los interceptores HTTP de Angular. Observador en todo RxJS. Estrategia siempre que sustituyes una implementación por token. Fábrica en useFactory. Adaptador al envolver un SDK de terceros. Citar dónde aparecen en el código que usas a diario impresiona bastante más que recitar la lista del libro.
Senior ¿Cuándo NO aplicarías un patrón?
Cuando el problema que resuelve no lo tienes todavía. Un patrón es una solución a un problema recurrente y viene con un coste: indirección, más archivos, más conceptos que aprender. Introducir una fábrica abstracta para una sola implementación, o un bus de eventos para dos módulos que se llaman directamente, no es previsión sino complejidad especulativa. La regla que suele funcionar es esperar a la tercera repetición antes de abstraer, y preferir un refactor cuando el problema aparezca de verdad. Un sistema sencillo que se puede cambiar es mejor que uno flexible que nadie entiende.
Medio ¿Qué es la idempotencia y por qué importa?
Una operación es idempotente si ejecutarla varias veces produce el mismo resultado que ejecutarla una. GET, PUT y DELETE deberían serlo por definición; POST no lo es. Importa porque las redes fallan: el cliente no sabe si el pago se procesó y la respuesta se perdió, o si nunca llegó. Sin idempotencia, reintentar puede cobrar dos veces. La solución estándar es una clave de idempotencia que el cliente envía en una cabecera y que el servidor almacena junto al resultado: si llega repetida, devuelve la respuesta guardada sin volver a ejecutar. Lo mismo aplica a los trabajos de una cola, que pueden entregarse más de una vez por diseño.
Senior ¿Qué es la consistencia eventual y cuándo la aceptas?
Que tras una escritura, los distintos componentes del sistema no vean el mismo valor de inmediato, pero converjan en poco tiempo. Aparece en cuanto hay réplicas de lectura, cachés, índices de búsqueda o comunicación entre servicios por eventos. Se acepta cuando el desfase es tolerable para el negocio: que el contador de tareas del panel tarde dos segundos en actualizarse no le importa a nadie. No se acepta donde una lectura obsoleta permite una acción incorrecta: comprobar saldo antes de un cobro, o verificar permisos. El truco de diseño es identificar esas pocas operaciones que exigen consistencia fuerte y dejar el resto en eventual.
Medio ¿Cómo abordas la deuda técnica?
Distinguiendo la deliberada de la accidental. La deliberada es una decisión legítima («salimos ahora sin caché porque necesitamos validar el producto») y debe anotarse con su motivo y su plan de pago. La accidental viene de no saber o de la prisa, y se detecta por síntomas: los mismos ficheros aparecen en todos los incidentes, cada estimación se multiplica por tres, nadie quiere tocar cierto módulo. La forma que funciona de pagarla es continua, aprovechando que ya tocas una zona por otro motivo, en lugar de pedir «un trimestre para refactorizar», que ningún responsable de producto aprueba y que además concentra todo el riesgo.
Medio ¿Qué buscas en una revisión de código?
Por orden: que resuelva el problema planteado y no otro; que no introduzca fallos de seguridad (validación, autorización, datos expuestos); que el diseño encaje con el resto del sistema y no cree acoplamientos raros; que tenga tests que fallarían si el código estuviera mal; y que se entienda al leerlo dentro de seis meses. Lo que no debería ocupar tiempo: el formato, que lo resuelve una herramienta, y las preferencias personales de estilo. Y una cuestión de forma que importa más de lo que parece: comentar sobre el código y no sobre la persona, y distinguir explícitamente lo que bloquea la aprobación de lo que es una sugerencia.
Medio ¿Qué significa Clean Code para ti en la práctica?
Sobre todo, nombres honestos: una función llamada obtenerUsuario que además actualiza la última conexión miente, y esa mentira costará una tarde a alguien. Después: funciones cortas con un solo nivel de abstracción, pocos parámetros (y un objeto si son muchos), evitar los booleanos como argumento porque en la llamada no se sabe qué significan, salidas tempranas en lugar de anidamiento profundo, y comentarios que expliquen el porqué y no el qué. Lo que el código no puede contar (una decisión de negocio rara, una limitación de un proveedor) merece un comentario; lo que ya dice el propio código, no.
Senior ¿Monolito modular o microservicios? Defiende tu elección.
Para casi todo, monolito modular: un solo despliegue, una transacción de base de datos que resuelve la consistencia gratis, depuración sencilla, y módulos con fronteras explícitas que permiten extraer un servicio el día que haga falta. Los microservicios se justifican por motivos organizativos (equipos que se bloquean entre sí al desplegar) o por escalado muy dispar de un componente concreto, y traen consigo latencia de red, consistencia eventual, transacciones distribuidas con sagas, y una factura de observabilidad que hay que pagar antes de empezar. La frase que suele cerrar bien la respuesta: no se puede dibujar la frontera correcta entre servicios antes de entender el dominio, y por eso empezar por microservicios suele producir un monolito distribuido, que es lo peor de ambos mundos.
Medio ¿Qué es un ADR?
Un registro de decisión de arquitectura: un documento breve, versionado junto al código, que recoge el contexto, las opciones consideradas, la decisión tomada y sus consecuencias. Su valor no está en el momento de escribirlo sino dos años después, cuando alguien pregunta por qué se eligió esa base de datos y la alternativa es la arqueología en el historial de Git o preguntar a gente que ya no está. Son inmutables: si la decisión cambia, se escribe uno nuevo que sustituye al anterior. Cuestan quince minutos y ahorran discusiones enteras.

23.7 DevOps y producción

Medio ¿Qué es una construcción multi-etapa de Docker y por qué se usa?
Usar una imagen con todas las herramientas de compilación para construir, y copiar solo el resultado a una imagen final mínima. La diferencia es enorme: una imagen con el SDK completo, las dependencias de desarrollo y el código fuente puede pasar de un gigabyte, mientras que la final con solo el runtime y los artefactos ronda las decenas o pocos cientos de megabytes. Y no es solo tamaño: menos contenido significa menos superficie de ataque y menos vulnerabilidades que parchear. Además conviene ordenar las capas para aprovechar la caché, copiando primero los ficheros de dependencias e instalándolas antes de copiar el código, que cambia mucho más a menudo.
Medio ¿Cómo gestionas los secretos en un despliegue?
Nunca en la imagen ni en el repositorio: una variable de entorno pasada en tiempo de construcción queda grabada en la capa y es recuperable. Se inyectan en tiempo de ejecución desde el gestor de secretos de la plataforma. Buenas prácticas adicionales: rotación periódica, permisos mínimos por servicio en lugar de una credencial compartida, y un escáner de secretos en el pipeline para detectar los que se cuelen en un commit. Y si un secreto se filtra, se rota inmediatamente: borrar el commit no sirve de nada porque ya está en el historial de todos los clones.
Medio ¿Qué etapas tiene tu pipeline de integración continua?
Instalación con caché de dependencias, análisis estático (formato, ESLint, comprobación de tipos), tests unitarios y de integración con una base de datos efímera en contenedor, compilación de los artefactos, escaneo de vulnerabilidades de dependencias e imagen, y publicación. Dos principios que conviene mencionar: ordenar las etapas de más rápida a más lenta para fallar cuanto antes, y que el pipeline sea determinista, porque un pipeline que falla de forma aleatoria enseña al equipo a reintentar sin mirar, y entonces deja de proteger nada.
Senior ¿Qué estrategias de despliegue conoces?
Recreación (parar y arrancar, con caída), actualización progresiva (se sustituyen las instancias por lotes, que es lo habitual en Kubernetes), azul/verde (dos entornos completos y un cambio de tráfico instantáneo, con reversión inmediata a cambio del doble de infraestructura) y canario (un porcentaje pequeño de tráfico a la versión nueva mientras se vigilan las métricas, escalando si todo va bien). La condición transversal, y lo que se está evaluando de verdad: durante cualquier despliegue progresivo conviven dos versiones del código contra el mismo esquema de base de datos, así que las migraciones deben ser compatibles hacia atrás.
Medio ¿Qué son los logs estructurados y por qué importan?
Registros en formato JSON con campos consistentes en lugar de texto libre. Importan porque permiten buscar y agregar: «dame todos los errores del usuario X en la última hora» es una consulta trivial sobre logs estructurados e imposible sobre texto plano. Los campos que no deberían faltar: marca de tiempo, nivel, identificador de correlación que atraviese todo el sistema, identificador de usuario, ruta y duración. Y una advertencia que suele valorarse: nunca registres contraseñas, tokens ni datos personales, y usa niveles con criterio, porque un sistema que registra todo en info es un sistema donde nadie encuentra nada.
Senior ¿Qué son los tres pilares de la observabilidad?
Logs (qué pasó en un punto concreto), métricas (agregados numéricos a lo largo del tiempo: peticiones por segundo, latencia por percentil, tasa de error) y trazas (el recorrido completo de una petición a través de los servicios, con el tiempo consumido en cada tramo). Se complementan: la métrica te dice que algo va mal, la traza te dice dónde, y el log te dice por qué. Sobre las métricas, un detalle que distingue: mira percentiles y no medias, porque una media de 200 ms puede ocultar que el 5 % de tus usuarios espera ocho segundos.
Senior Producción está caída. ¿Qué haces?
Primero restablecer el servicio, luego investigar; son dos fases distintas y mezclarlas alarga la caída. En concreto: confirmar el alcance real (¿todos los usuarios o uno?), revisar qué cambió recientemente porque la inmensa mayoría de las incidencias siguen a un despliegue o a un cambio de configuración, y revertir si eso restablece el servicio aunque no entiendas aún la causa. Comunicar pronto y con honestidad, designar a alguien que coordine y a alguien que investigue para no pisarse, y anotar lo que se va probando. Después, un análisis sin culpables centrado en por qué el sistema permitió el fallo y qué comprobación automática lo habría detectado antes. Culpar a una persona garantiza que la próxima vez nadie cuente lo que pasó.
Medio ¿Cómo escalarías esta aplicación si el tráfico se multiplica por diez?
Midiendo primero dónde está el límite, que casi nunca es donde uno cree. El orden habitual: CDN y caché para el contenido estático y las respuestas cacheables, que suele quitar la mayor parte de la carga; escalado horizontal de la API, que requiere que sea sin estado (nada de sesiones en memoria ni ficheros en disco local); revisión de las consultas y los índices, porque la base de datos es el cuello de botella más común; réplicas de lectura para los informes; y colas para sacar de la petición todo lo que no tenga que ser síncrono. Escalar la base de datos en escritura, con particionado o sharding, es lo último porque es lo más caro y lo más difícil de revertir.

23.8 Preguntas de diseño de sistemas

Guion para cualquier pregunta de diseño

1. Aclarar requisitos. Nunca empieces a dibujar cajas: pregunta por el volumen esperado, la proporción entre lecturas y escrituras, la latencia aceptable, si se necesita consistencia fuerte y qué está fuera del alcance. Las preguntas de diseño son deliberadamente ambiguas y una parte de lo que se evalúa es si lo detectas.

2. Estimar magnitudes. Cifras aproximadas de usuarios, peticiones por segundo y almacenamiento. No hace falta precisión, hace falta saber si hablamos de una máquina o de cien.

3. Definir la API y el modelo de datos, que es donde se ve de verdad si entiendes el problema.

4. Diseño general con los componentes principales y el flujo de una operación de extremo a extremo.

5. Profundizar en la parte más interesante, normalmente la que el entrevistador señale.

6. Cuellos de botella y compromisos: qué falla primero al crecer y qué harías entonces.

23.8.1 Diseña un acortador de URL

Qué evalúa: generación de identificadores, relación entre lecturas y escrituras, caché y diseño de un esquema sencillo bajo carga alta.

Puntos clave. La proporción lectura/escritura es brutalmente asimétrica (miles de redirecciones por cada alta), así que todo el diseño gira en torno a servir la lectura desde caché. Para el código: codificar en base 62 un contador o un identificador generado, frente a un hash de la URL, que obliga a gestionar colisiones; explica el compromiso entre longitud y espacio de nombres. La redirección debe usar 301 o 302 según si quieres poder contar las visitas (301 se cachea en el navegador y dejarás de verlas). Añade caducidad, URL personalizadas, y protección contra el uso para distribuir enlaces maliciosos.

Errores típicos: proponer un autoincremental visible sin más (permite enumerar todos los enlaces del sistema), olvidar la caché, y no mencionar qué pasa cuando el código no existe.

23.8.2 Diseña las notificaciones en tiempo real de TaskFlow

Qué evalúa: comunicación asíncrona, escalado de conexiones persistentes y entrega fiable.

Puntos clave. Empieza justificando la tecnología: eventos del servidor si el flujo es solo de bajada y quieres simplicidad, WebSockets si hay bidireccionalidad, sondeo si el volumen es bajo y quieres cero infraestructura nueva. Al escalar, el problema central es que una conexión vive en una instancia concreta, de modo que hace falta un adaptador con Redis para distribuir los mensajes. Trata la entrega cuando el usuario está desconectado: almacenar las notificaciones y entregarlas al reconectar, con confirmación de lectura. Y la reconexión con retroceso exponencial más recuperación del estado perdido.

Errores típicos: asumir una sola instancia, no tratar la desconexión, y autenticar solo en el handshake sin plan para cuando el token caduque en una conexión de horas.

23.8.3 Diseña la exportación de informes pesados

Qué evalúa: procesamiento asíncrono, sondeo frente a notificación y gestión de ficheros.

Puntos clave. La respuesta correcta empieza por no hacerlo dentro de la petición HTTP: el endpoint encola el trabajo y responde 202 Accepted con un identificador y una URL de estado. Un worker genera el fichero por streaming (nunca cargando un millón de filas en memoria), lo sube a almacenamiento de objetos y marca el trabajo como listo. El cliente sondea el estado o recibe un aviso, y descarga mediante una URL firmada con caducidad, no a través de tu API. Añade: idempotencia por si el usuario pulsa dos veces, límite de exportaciones concurrentes por usuario, y limpieza de ficheros antiguos.

Errores típicos: generar el fichero en memoria, servirlo desde la API consumiendo un proceso durante minutos, y no limitar el número de peticiones simultáneas.

23.8.4 Diseña la multi-tenencia de TaskFlow

Qué evalúa: aislamiento de datos, seguridad y compromisos operativos.

Puntos clave. Tres modelos, de menor a mayor aislamiento: base de datos compartida con columna de inquilino (barato y simple, pero un WHERE olvidado filtra datos entre clientes), esquema por inquilino (mejor aislamiento, complica las migraciones porque hay que aplicarlas a N esquemas) y base de datos por inquilino (aislamiento máximo, coste operativo alto, razonable con pocos clientes grandes). Con el modelo compartido, la defensa seria es Row Level Security en el motor, más un filtro global del ORM, más tests que intenten explícitamente acceder a datos de otro inquilino. Menciona también el ruido entre vecinos: un cliente pesado degradando al resto, y cómo lo limitarías.

Errores típicos: confiar solo en el filtro de la aplicación, y olvidar que las exportaciones, los informes y los trabajos en segundo plano también deben respetar el aislamiento.

23.8.5 Diseña la búsqueda de tareas con filtros y paginación estable

Qué evalúa: diseño de API, índices, paginación y honestidad sobre los límites de una base de datos relacional.

Puntos clave. Define primero el contrato: qué filtros se combinan, qué ordenaciones se permiten (lista blanca, nunca un campo libre del cliente) y qué límite máximo de página aceptas. Paginación por cursor con desempate único para que sea estable mientras se insertan tareas. Índices compuestos alineados con las combinaciones de filtros más usadas, aceptando que no puedes indexar todas las combinaciones posibles. Para la búsqueda por texto, empieza con la búsqueda de texto completo del propio motor y plantea un índice externo solo si los requisitos (relevancia, facetas, tolerancia a errores) lo justifican, sin olvidar que eso introduce sincronización y consistencia eventual.

Errores típicos: permitir ordenar por cualquier columna que mande el cliente, no limitar el tamaño de página, y proponer Elasticsearch antes de haber intentado un índice.

23.9 Preguntas de criterio (las que empiezan por «depende»)

Senior ¿El ORM siempre es mejor que escribir SQL?
No, y quien responda que sí revela poca experiencia. El ORM gana en el 90 % del trabajo cotidiano: mapeo, seguridad frente a inyección, productividad y refactorizaciones seguras. Pierde en las consultas analíticas complejas, en las actualizaciones masivas y en cualquier caso donde necesites control fino del plan de ejecución; ahí el SQL directo es más claro y más rápido. Lo sano es usar el ORM por defecto y bajar a SQL de forma deliberada y localizada, sabiendo que entonces te saltas el Unit of Work y los filtros globales.
Senior ¿Las señales sustituyen a RxJS?
No. Sustituyen a RxJS en un uso concreto para el que RxJS era excesivo: mantener y derivar estado síncrono para la vista, donde un BehaviorSubject con map y combineLatest era mucha maquinaria. Donde RxJS sigue siendo insustituible es en la orquestación de eventos en el tiempo: cancelación, debounce, reintentos, combinación de flujos, contrapresión. La arquitectura razonable hoy combina ambos: RxJS en la frontera asíncrona, señales para el estado que se pinta, y conversión explícita entre los dos mundos.
Senior ¿Los microservicios escalan mejor?
Escalan mejor los equipos, no necesariamente el sistema. Un monolito bien hecho se escala horizontalmente poniendo más réplicas detrás de un balanceador, y suele rendir más porque una llamada a un método no cuesta lo que una petición de red. Lo que los microservicios permiten es escalar de forma independiente un componente concreto y desplegar sin coordinar con otros equipos. Si tienes ocho personas y un producto, casi seguro que tu problema no es ese, y estarás pagando latencia y consistencia eventual a cambio de nada.
Medio ¿El 100 % de cobertura garantiza calidad?
No, y puede ser contraproducente. La cobertura mide qué líneas se ejecutaron, no si comprobaste algo útil: un test sin una sola aserción da cobertura completa. Perseguir el número lleva a cubrir lo trivial, que es barato, y dejar sin probar la lógica compleja, que es cara. Un 70 % concentrado en la lógica de negocio y en los recorridos críticos vale mucho más que un 95 % repartido por getters. La métrica que sí correlaciona con calidad es otra: que cada corrección de fallo venga con el test que lo reproduce.
Medio ¿Hay que usar siempre TypeScript estricto?
En un proyecto nuevo, sí, sin discusión: activarlo cuesta nada al principio y elimina familias enteras de errores. En un proyecto grande heredado en JavaScript, activar todo de golpe produce miles de errores y bloquea el desarrollo, así que se hace por fases: primero strictNullChecks, que es el que más valor aporta, con excepciones acotadas por carpeta, y se avanza a medida que se tocan los módulos. Lo que no funciona es la vía intermedia permanente: un proyecto con strict a medias y any por todas partes tiene el coste de los tipos sin sus beneficios.
Senior ¿Renderizado en servidor siempre?
Depende de para quién es la aplicación. Si es pública y el posicionamiento en buscadores o el primer pintado importan (comercio electrónico, contenidos, páginas de producto), el SSR o el prerenderizado son casi obligatorios. Si es una herramienta interna tras un login, donde nadie la indexa y los usuarios la tienen abierta todo el día, el SSR añade complejidad operativa (un servidor Node que mantener, código que debe funcionar en ambos entornos, problemas de hidratación) a cambio de un beneficio marginal. Y hay una opción intermedia que se olvida: prerenderizar solo las rutas públicas.
Medio ¿Mejor NgRx o un servicio con señales?
Depende del tamaño y de la naturaleza del estado. Un servicio con señales cubre perfectamente la mayoría de aplicaciones y tiene una fracción del código y de la curva de aprendizaje. NgRx aporta cuando hay mucho estado compartido entre muchas vistas, cuando necesitas trazabilidad de cada cambio, herramientas de depuración con viaje en el tiempo, o cuando el equipo es grande y el rigor de un flujo unidireccional estricto evita que cada uno gestione el estado a su manera. Adoptarlo «porque es lo profesional» en una aplicación de diez pantallas es introducir ceremonia sin beneficio.
Senior ¿Es mejor guardar el JWT en localStorage o en una cookie?
Depende del modelo de amenaza, y la respuesta completa está en la sección de NestJS. En resumen: la cookie HttpOnly protege frente a la exfiltración por XSS pero abre CSRF, que se mitiga con SameSite; localStorage evita CSRF pero queda expuesto a cualquier XSS. El consenso práctico es cookie para el refresco y memoria para el acceso. Y el matiz que cierra bien: con un XSS activo el atacante puede actuar en nombre del usuario aunque no lea el token, así que la prioridad real es no tener XSS.
Medio ¿Hay que testear todo?
No: hay que probar el riesgo. La lógica de negocio, los cálculos, los permisos y los recorridos críticos, sí y a conciencia. Un componente que solo pinta una entrada, o un fichero de configuración, aportan poco frente a lo que cuesta mantener esos tests. Y hay un coste que se olvida: cada test es código que hay que actualizar en cada refactor, así que un test de bajo valor tiene retorno negativo. La pregunta útil ante cada caso es «si esto se rompiera, ¿cuánto tardaríamos en enterarnos y cuánto dolería?».
Senior ¿Conviene usar siempre la última versión del framework?
Conviene mantenerse cerca, pero no en el filo. Quedarse atrás es lo peor: las actualizaciones se acumulan, dejas de recibir parches de seguridad y llega el día en que migrar es un proyecto de meses. Estar en el filo el primer día implica encontrarte los fallos antes que nadie y esperar a que el ecosistema se ponga al día. El equilibrio habitual es actualizar unas semanas después de cada versión mayor, con tests que respalden el cambio, aprovechando las migraciones automáticas del CLI, y de forma regular en lugar de a saltos.

23.10 Ejercicios de código en vivo

Lo que se evalúa mientras programas delante de alguien Casi nunca es la solución óptima. Es si preguntas antes de empezar, si dices en voz alta lo que estás pensando, si empiezas por algo que funcione antes de optimizar, si reconoces cuando te has equivocado y si aceptas una pista sin ponerte a la defensiva. Un candidato que se atasca pero razona en voz alta puntúa por encima de uno que resuelve en silencio y no sabe explicar por qué.
CV-01 · Implementa un debounce

Escribe una función debounce(fn, ms) que retrase la ejecución hasta que pasen ms sin nuevas llamadas. Se observa: si manejas correctamente this y los argumentos, si tipas bien la función genérica y si mencionas la variante con ejecución inmediata al principio.

Solución comentada
function debounce<T extends (...args: never[]) => void>(fn: T, ms: number) {
  let id: ReturnType<typeof setTimeout> | undefined;

  // function normal, no flecha: así conserva el 'this' de la llamada
  return function (this: unknown, ...args: Parameters<T>) {
    if (id !== undefined) clearTimeout(id);
    id = setTimeout(() => fn.apply(this, args), ms);
  };
}
// Repregunta esperable: ¿y cómo lo cancelas?
// Se devuelve un objeto con la función y un método cancel() que hace clearTimeout.
CV-02 · Encuentra y arregla la fuga de memoria
@Component({ selector: 'app-panel', template: '...' })
export class PanelComponent implements OnInit {
  datos: Task[] = [];
  constructor(private ws: WebSocketService, private api: TaskService) {}

  ngOnInit() {
    this.ws.mensajes$.subscribe(m => this.datos.push(m));
    setInterval(() => this.api.refrescar().subscribe(), 5000);
    window.addEventListener('resize', this.alRedimensionar);
  }
  alRedimensionar = () => { /* ... */ };
}

Se observa: si identificas las tres fugas, si sabes cuáles no lo son y si conoces las soluciones idiomáticas.

Solución comentada
// Hay TRES fugas: la suscripción al WebSocket (flujo infinito),
// el setInterval y la escucha de resize. La suscripción a this.api.refrescar()
// NO fuga: una petición HTTP se completa sola.

@Component({ selector: 'app-panel', template: '...' })
export class PanelComponent {
  private readonly ws = inject(WebSocketService);
  private readonly api = inject(TaskService);
  private readonly destroyRef = inject(DestroyRef);

  readonly datos = signal<Task[]>([]);

  constructor() {
    // 1) takeUntilDestroyed cancela al destruirse el componente
    this.ws.mensajes$
      .pipe(takeUntilDestroyed())
      .subscribe(m => this.datos.update(d => [...d, m]));

    // 2) timer de RxJS en lugar de setInterval: se cancela igual
    timer(0, 5000)
      .pipe(switchMap(() => this.api.refrescar()), takeUntilDestroyed())
      .subscribe();

    // 3) la escucha se registra y se retira explícitamente
    const alRedimensionar = () => { /* ... */ };
    window.addEventListener('resize', alRedimensionar);
    this.destroyRef.onDestroy(() =>
      window.removeEventListener('resize', alRedimensionar));
  }
}
CV-03 · Detecta y corrige un N+1
const proyectos = await em.find(Project, { teamId });
for (const p of proyectos) {
  const tareas = await p.tasks.loadItems();     // ← una consulta por proyecto
  p.pendientes = tareas.filter(t => !t.done).length;
}

Se observa: si reconoces el patrón, si propones más de una solución y si sabes cuál conviene según el tamaño de los datos.

Solución comentada
// Opción A: cargar todo de una vez (bien si el número de tareas es moderado)
const proyectos = await em.find(Project, { teamId }, { populate: ['tasks'] });

// Opción B: si SOLO necesitas el contador, no traigas las tareas.
// Una única consulta agregada es muchísimo más barata:
const filas = await em.getConnection().execute<{ project_id: string; n: number }[]>(
  `SELECT project_id, count(*)::int AS n
     FROM tasks
    WHERE project_id = ANY(?) AND status <> 'done'
    GROUP BY project_id`,
  [proyectos.map(p => p.id)],
);
// Opción C: contador desnormalizado mantenido por trigger o suscriptor,
// si esta consulta se hace en cada carga de la pantalla principal.
//
// El criterio: A si necesitas los datos, B si solo necesitas el agregado,
// C si el agregado se pide constantemente y toleras mantenerlo.
CV-04 · Escribe un guard de roles

Implementa @Roles() y el guard que lo consume en NestJS, contemplando las rutas públicas. Se observa: uso de Reflector, tratamiento del caso sin metadatos y decisión entre devolver false o lanzar una excepción.

Solución comentada
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Rol[]) => SetMetadata(ROLES_KEY, roles);

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(ctx: ExecutionContext): boolean {
    // getAllAndOverride: el método tiene prioridad sobre el controlador
    const requeridos = this.reflector.getAllAndOverride<Rol[]>(ROLES_KEY, [
      ctx.getHandler(),
      ctx.getClass(),
    ]);
    // Sin metadatos, la ruta no exige rol concreto: dejar pasar.
    if (!requeridos?.length) return true;

    const { user } = ctx.switchToHttp().getRequest();
    if (!user) throw new UnauthorizedException();     // 401: no sé quién eres

    const permitido = requeridos.some(r => user.roles.includes(r));
    // 403 y no false: el mensaje al cliente es más honesto y depurar es más fácil.
    if (!permitido) throw new ForbiddenException('Permisos insuficientes');
    return true;
  }
}
CV-05 · Tipa una función genérica

Escribe agruparPor(items, clave) que agrupe un array por una propiedad, con tipos correctos y sin any. Se observa: uso de keyof, restricciones de tipo y Record.

Solución comentada
function agruparPor<T, K extends keyof T>(
  items: readonly T[],
  clave: K,
): Map<T[K], T[]> {
  const mapa = new Map<T[K], T[]>();
  for (const item of items) {
    const k = item[clave];
    const grupo = mapa.get(k);
    if (grupo) grupo.push(item);
    else mapa.set(k, [item]);
  }
  return mapa;
}

// agruparPor(tareas, 'status')  →  Map<TaskStatus, Task[]>
// agruparPor(tareas, 'inexistente')  →  error de compilación
//
// Se usa Map y no un objeto porque la clave puede no ser string,
// y porque un objeto literal arrastra el prototipo (riesgo de colisión
// con claves como 'constructor').
CV-06 · Encuentra el error de transacción
async completarProyecto(id: string) {
  const proyecto = await this.em.findOneOrFail(Project, id);
  proyecto.completedAt = new Date();
  await this.em.flush();

  const tareas = await this.em.find(Task, { project: id, status: 'open' });
  for (const t of tareas) t.status = 'cancelled';
  await this.em.flush();

  await this.correo.enviarResumen(proyecto);
}

Se observa: si detectas la atomicidad rota y el efecto secundario dentro del flujo.

Solución comentada
// Dos problemas. 1) Dos flush independientes: si el segundo falla, el proyecto
// queda completado con tareas abiertas. 2) El correo se envía aunque después
// falle algo, y no se puede deshacer.

async completarProyecto(id: string) {
  const proyecto = await this.em.transactional(async (em) => {
    const p = await em.findOneOrFail(Project, id);
    p.completedAt = new Date();

    const tareas = await em.find(Task, { project: id, status: 'open' });
    for (const t of tareas) t.status = 'cancelled';

    return p;              // un único flush implícito al confirmar
  });

  // El efecto externo se lanza DESPUÉS de confirmar, y como trabajo en cola
  // para que un fallo del proveedor de correo no afecte a la operación
  // ni deje la transacción abierta esperando a una llamada de red.
  await this.cola.add('resumen-proyecto', { proyectoId: proyecto.id });
  return proyecto;
}
CV-07 · Arregla la condición de carrera

Dos usuarios asignan a la vez la última plaza disponible de un proyecto y ambos lo consiguen. El código lee el contador, comprueba el límite y guarda. Corrígelo de dos formas distintas.

Solución comentada
-- Forma 1: actualización atómica con la condición en el propio UPDATE.
-- La base de datos garantiza que solo una transacción lo consigue.
UPDATE projects
   SET miembros = miembros + 1
 WHERE id = ? AND miembros < max_miembros
RETURNING miembros;
-- Si no devuelve ninguna fila, el proyecto estaba lleno → 409.

-- Forma 2: bloqueo pesimista, si además hay que hacer más comprobaciones.
BEGIN;
SELECT * FROM projects WHERE id = ? FOR UPDATE;   -- los demás esperan
-- comprobar y actualizar con seguridad
COMMIT;

-- Forma 3 (bonus): bloqueo optimista con columna de versión y reintento.
-- Elegiría la 1 por sencillez y porque no mantiene bloqueos.
CV-08 · Refactoriza a algo testeable

Te dan un método de 80 líneas que consulta la base de datos, aplica reglas de negocio, formatea texto, envía un correo y devuelve un DTO. Descríbelo en voz alta y refactorízalo. Se observa: si identificas las responsabilidades, si extraes la lógica pura primero y si justificas cada extracción por su beneficio y no por dogma.

23.11 Preguntas que deberías hacer tú

Madurez técnica

  • ¿Cómo es el proceso desde que se decide una funcionalidad hasta que está en producción?
  • ¿Cuántas veces desplegáis a la semana y cuánto tarda un despliegue?
  • ¿Qué pasa cuando algo se rompe en producción a las tres de la tarde?
  • ¿Qué cobertura de tests tenéis y quién los escribe?
  • ¿Cuál es la parte del código que nadie quiere tocar y por qué?

Señales de alarma: «desplegamos una vez al mes por la noche», «los tests los hace QA al final», «no tenemos entorno de pruebas».

Equipo y producto

  • ¿Cómo se decide en qué se trabaja? ¿Quién prioriza?
  • ¿Cómo son las revisiones de código y cuánto tardan en aprobarse?
  • ¿Qué le pasó a la última persona que ocupó este puesto?
  • ¿Cuánto tiempo se dedica a mantenimiento frente a funcionalidades nuevas?
  • ¿Cómo sería un buen primer trimestre para vosotros en este puesto?

Señales de alarma: respuestas evasivas sobre la rotación, «aquí todos hacemos de todo» sin matizar, o incomodidad al preguntar por las horas.

23.12 Simulacros cronometrados

SimulacroEstructuraQué se evalúa
Junior · 30 min5 min de presentación y tu proyecto · 10 min de JavaScript y TypeScript (secciones 23.2) · 10 min de Angular básico (ciclo de vida, componentes, DI) · 5 min de tus preguntasFundamentos sólidos y capacidad de explicar lo que has hecho
Medio · 45 min5 min de presentación · 10 min de Angular avanzado (reactividad, rendimiento) · 10 min de NestJS (ciclo de petición, seguridad) · 10 min de datos (N+1, transacciones, índices) · 5 min de un ejercicio en vivo · 5 min de tus preguntasCriterio, capacidad de diagnóstico y experiencia real
Senior · 60 min5 min de presentación · 15 min de diseño de sistemas (sección 23.8) · 10 min de arquitectura y compromisos · 10 min de una incidencia de producción · 10 min de profundidad técnica a elección · 5 min de liderazgo técnico y mentoría · 5 min de tus preguntasJuicio, comunicación e influencia en el equipo

Rúbrica para autoevaluarte

  • Corrección técnica: ¿la respuesta era cierta, incluidos los matices?
  • Estructura: ¿definición, problema, ejemplo y matiz, o divagación?
  • Concisión: ¿respondiste en menos de 90 segundos o te fuiste por las ramas?
  • Honestidad: ¿dijiste «no lo sé» cuando tocaba en lugar de improvisar?
  • Ejemplos propios: ¿apoyaste la respuesta en algo que has hecho de verdad?
  • Repreguntas: ¿aguantaste el «¿y por qué?» dos niveles más abajo?

Grábate en audio respondiendo diez preguntas al azar y escúchate. Es incómodo y es con diferencia lo que más rápido mejora el resultado.

23.13 Repaso de las últimas 48 horas

Repasa esto

  • El orden del ciclo de petición de NestJS y qué hace cada pieza.
  • Señales frente a RxJS y cuándo usar cada uno.
  • La detección de cambios y qué implica OnPush.
  • Unit of Work, Identity Map y qué ocurre en un flush.
  • El N+1: cómo se detecta y las tres formas de resolverlo.
  • JWT: dónde se guarda, cómo se refresca y cómo se revoca.
  • SOLID con un ejemplo propio de cada principio.
  • Tu propio proyecto: arquitectura, decisiones y qué harías distinto.

No hagas esto

  • Estudiar un tema nuevo la noche anterior: solo genera ansiedad.
  • Memorizar respuestas palabra por palabra; se nota y se cae a la primera repregunta.
  • Presentarte sin haber mirado el producto y el equipo de la empresa.
  • Preparar solo lo técnico y no las dos o tres historias de proyectos que vas a contar.
  • Dormir cuatro horas para repasar más.

23.14 Recursos adicionales y cierre

Cierre del libro

Has llegado al final de veintitrés capítulos, pero conviene decir lo obvio: ninguna cantidad de lectura sustituye a haber construido y mantenido algo real. Las respuestas que impresionan en una entrevista no son las que se recitan, sino las que empiezan por «nos pasó justamente eso y lo resolvimos así, aunque con el tiempo vimos que...». Esa clase de respuesta solo se consigue de una manera.

Si aún no lo has hecho, vuelve al capítulo 22 y construye el proyecto integrador de principio a fin: con su base de datos, su autenticación, sus tests, su contenedor y su despliegue. Rómpelo a propósito, diagnostícalo y arréglalo. Ese proyecto, y la capacidad de explicar cada decisión que tomaste en él, valen más que este libro entero.