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.
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.
23.1.2 Qué evalúa realmente el entrevistador
| Lo que parece que se evalúa | Lo que se evalúa de verdad |
|---|---|
| Si te sabes la definición | Si entiendes qué problema resuelve y cuándo no usarlo |
| Si conoces la última versión | Si sabes distinguir lo que cambia de lo que permanece |
| Si respondes rápido | Si preguntas antes de responder cuando el enunciado es ambiguo |
| Si aciertas el ejercicio | Cómo razonas cuando te atascas y si aceptas una pista |
| Cuánta tecnología dominas | Si serías agradable de tener al lado durante dos años |
23.1.3 Plan de preparación según el puesto
- Junior. Domina los fundamentos de JavaScript y TypeScript, el ciclo de vida de un componente, qué es la inyección de dependencias y cómo funciona una petición HTTP de principio a fin. Que sepas explicar tu propio proyecto con detalle vale más que citar diez librerías.
- Nivel medio. Se espera criterio: por qué elegiste señales en un sitio y RxJS en otro, cómo detectas un N+1, cómo estructuras los DTO, qué pruebas y qué no. Prepara historias reales de problemas que hayas resuelto.
- Senior. Se evalúa juicio arquitectónico y capacidad de explicar compromisos: qué se gana y qué se pierde con cada decisión, cómo migrarías sin caída, cómo diagnosticarías una incidencia en producción y cómo influyes en el equipo. Las preguntas de diseño de sistemas y las de «depende» pesan más que cualquier detalle de API.
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?
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?
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?
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?
{...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?
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?
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?
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.
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?
@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?
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?
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?
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?
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?
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?
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?
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?
@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ó?
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.
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?
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?
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?
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?
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?
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?
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?
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?
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?
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?
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?
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?
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?
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.
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?
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?
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?
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?
Medio ¿Qué es el modelo contenedor/presentación?
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?
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?
[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?
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?
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?
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?
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?
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?
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?
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?
23.4 NestJS
Junior ¿Qué aporta NestJS frente a Express puro?
Junior ¿Qué es un módulo y cómo se organizan?
@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?
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.
Medio ¿Middleware, guard o interceptor? ¿Cómo eliges?
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?
@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?
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?
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?
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?
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?
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?
@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?
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?
Medio ¿Cómo implementas autorización basada en roles?
@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?
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?
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?
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?
Senior ¿REST o GraphQL?
Senior ¿Cuándo pasarías a microservicios?
Medio ¿Cómo versionas una API?
/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?
.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?
Senior ¿Express o Fastify como adaptador?
Medio ¿Cómo testeas en NestJS?
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?
Medio ¿Cómo implementas la salud del servicio y qué expones?
23.5 MikroORM y bases de datos
Medio ¿Data Mapper o Active Record?
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.
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?
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?
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?
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()?
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?
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?
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?
Medio ¿Cómo implementas el borrado lógico?
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?
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?
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?
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?
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?
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?
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?
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?
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?
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.
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?
Senior ¿Qué es la arquitectura hexagonal?
Senior ¿Qué te llevas de DDD a un proyecto normal?
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?
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?
Medio ¿Qué es la idempotencia y por qué importa?
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?
Medio ¿Cómo abordas la deuda técnica?
Medio ¿Qué buscas en una revisión de código?
Medio ¿Qué significa Clean Code para ti en la práctica?
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.
Medio ¿Qué es un ADR?
23.7 DevOps y producción
Medio ¿Qué es una construcción multi-etapa de Docker y por qué se usa?
Medio ¿Cómo gestionas los secretos en un despliegue?
Medio ¿Qué etapas tiene tu pipeline de integración continua?
Senior ¿Qué estrategias de despliegue conoces?
Medio ¿Qué son los logs estructurados y por qué importan?
info es un sistema donde nadie
encuentra nada.Senior ¿Qué son los tres pilares de la observabilidad?
Senior Producción está caída. ¿Qué haces?
Medio ¿Cómo escalarías esta aplicación si el tráfico se multiplica por diez?
23.8 Preguntas de diseño de sistemas
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?
Senior ¿Las señales sustituyen a RxJS?
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?
Medio ¿El 100 % de cobertura garantiza calidad?
Medio ¿Hay que usar siempre TypeScript estricto?
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?
Medio ¿Mejor NgRx o un servicio con señales?
Senior ¿Es mejor guardar el JWT en localStorage o en una 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?
Senior ¿Conviene usar siempre la última versión del framework?
23.10 Ejercicios de código en vivo
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.
@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));
}
}
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.
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;
}
}
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').
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;
}
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.
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
| Simulacro | Estructura | Qué se evalúa |
|---|---|---|
| Junior · 30 min | 5 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 preguntas | Fundamentos sólidos y capacidad de explicar lo que has hecho |
| Medio · 45 min | 5 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 preguntas | Criterio, capacidad de diagnóstico y experiencia real |
| Senior · 60 min | 5 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 preguntas | Juicio, 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
- angular.dev · docs.nestjs.com · mikro-orm.io — la documentación oficial sigue siendo la mejor preparación técnica.
- System Design Primer — para las preguntas de diseño, aunque esté orientado a sistemas más grandes.
- OWASP Top 10 — las preguntas de seguridad salen de aquí con muchísima frecuencia.
- roadmap.sh · Full Stack — útil para detectar huecos, no para estudiar linealmente.
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.