Biblia — Front Semi Sr Angular · Capgemini

Angular 20+ · Signals · RxJS · Reactive Forms · Componentes reutilizables · HTTP · Rendimiento

Plan de estudio

Cinco pestañas, en este orden de uso: aquí decides qué estudias hoy → Respuestas para aprender qué decir → Ejercicios para teclearlo → Sobre ti para tu historia → Repaso para comprobar que se quedó.

Temporizador Pomodoro

Enfoque
25:00
Pomodoros hoy: 0

25 min de enfoque → 5 de descanso. Cada 4 pomodoros, descanso de 20. Sigue corriendo arriba a la derecha aunque cambies de pestaña.

Cómo usar cada pomodoro

MinutoQué haces
0 – 2Escribe en una línea el objetivo: "Teclear el buscador con switchMap sin mirar". Un objetivo por pomodoro.
2 – 20Trabajo activo: teclear un ejercicio o responder en voz alta. Nada de solo leer.
20 – 25Hoja en blanco: cierra todo y escribe o di lo que recuerdas del tema. Luego abre y compara. Lo que faltó es lo que repasas mañana.
DescansoLejos de la pantalla: párate, agua, camina. No redes sociales (el cerebro no descansa).

Las 6 técnicas que usa este plan

TécnicaPor qué funcionaCómo la aplicas aquí
Recuperación activaRecordar fortalece la memoria mucho más que releer.Pestaña Repaso: lees la pregunta, respondes en voz alta y después abres la respuesta.
Repetición espaciadaRepasar justo cuando empiezas a olvidar fija lo aprendido.Hoy marcas "Repasar" lo que dudes; esas tarjetas las repasas antes de dormir y otra vez el día de la entrevista.
GeneraciónLo que produces tú se recuerda más que lo que copias.Los ejercicios se teclean de cero. La solución solo se abre al terminar o después de 10 min atorado.
Técnica FeynmanSi puedes explicarlo simple, lo entiendes.Explica cada respuesta en voz alta como si se la contaras a un junior. Si te trabas, ahí está el hueco.
IntercaladoMezclar temas entrena a elegir la herramienta correcta, como en la entrevista.Botón "Mezclar" en las tarjetas y el simulacro con preguntas al azar.
Ensayo en voz alta grabadoOírte revela muletillas y respuestas largas.Graba el simulacro con el celular, escúchalo y rehace las 3 peores respuestas.

Plan exprés — entrevista el viernes o el lunes

Hoy jueves haces lo que más probablemente te van a preguntar. Si la entrevista es el viernes, con eso vas. Si es el lunes, el fin de semana completas el resto y construyes el proyecto de Java.

🔴 Hoy jueves — lo indispensable (7 pomodoros ≈ 3.5 h)

  1. Sobre ti: decide tu respuesta de modalidad y tu rango salarial. Di "Cuéntame de ti" 3 veces en voz alta, cronometrado. (La reclutadora pregunta esto primero; si fallas aquí, no llegas a lo técnico.)
  2. Respuestas 1–3: estructura del proyecto, Signals y RxJS, en voz alta. Cada una en 20–30 segundos: concepto y ejemplo en el mismo bloque.
  3. Respuestas 4–7: Reactive Forms, componentes reutilizables, interceptores y rendimiento.
  4. Ejercicios D6 y D7: el buscador y el botón sin doble envío. Es lo que más probablemente te pidan teclear.
  5. Ejercicios D11 y D3: el formulario con líneas dinámicas y la tarjeta padre-hijo.
  6. Respuestas 8–11 (testing, Java, comandos, Git) + pasar todas las tarjetas de Repaso una vez, marcando "Repasar" las que dudes.
  7. Proyecto Java: córrelo, prueba sus endpoints en Swagger y lee los archivos con el LEEME.md. Si te sobra tiempo, haz el cambio tuyo que ahí se indica.

🌙 Antes de dormir: 15 minutos solo con las tarjetas marcadas "Repasar". Dormir bien es parte del estudio: ahí se fija lo que repasaste.

🟠 Si la entrevista es el viernes — mañana (2 pomodoros)

  1. Tarjetas "Repasar" + "Cuéntame de ti" una vez + tus 3 preguntas para ellos.
  2. Simulacro rápido: 10 preguntas al azar del simulacro, en voz alta. Luego el checklist del día.

Una hora antes, nada nuevo. En Java ya puedes hablar de tu proyecto, si lo corriste y lo leíste.

🟢 Si es el lunes — el fin de semana completas

  1. Viernes: ejercicios D1, D2, D5, D9 y D13 (3 pomodoros) + simulacro completo grabado; escúchalo y rehaz las 3 respuestas más flojas.
  2. Sábado: rehaz tú el proyecto de Java desde cero con J1, J2 y J3 (3 pomodoros), usando el de Desktop\farmacia-api como referencia. Tarjetas "Repasar".
  3. Domingo: J4, J5 y J6 (3 pomodoros). Ejercicios D8, D10, D12, D14 y D15 si te da tiempo.
  4. Lunes: como el plan del viernes: tarjetas, "Cuéntame de ti", simulacro corto y checklist.

Respuestas técnicas

Cada respuesta es un solo bloque de unas tres frases, dicho en 20–30 segundos: el concepto en una frase y, pegado a él, dónde lo usaste y qué resolvía. El ejemplo demuestra el concepto: se oye como alguien que lo aplicó, no como alguien que lo memorizó. Si quieren más, repreguntan; para eso está "Si repreguntan".

Todo sale del historial de git de tus dos proyectos. "Creé" solo donde el archivo es tuyo; "trabajé en" o "en el punto de venta" donde es código del equipo. Las respuestas en naranja no tienen caso real en producción: dilas tal cual, sin inventar.

1 · Arquitectura modular — Core, Shared, Feature

¿Cómo estructuras un proyecto Angular?

Respuesta Lo divido en tres carpetas: core, con lo que existe una sola vez, como el guard que no deja entrar sin sesión y los interceptores; shared, con lo reutilizable, como los modales de buscar medicamento o cobrar que se abren desde cualquier pantalla; y features, una por área de negocio con lazy loading, como tienda, pago y entrega. Así está el punto de venta donde trabajé, con la regla de que un feature nunca importa de otro. En el backoffice lo llevamos más lejos: cada área es un microfrontend que se despliega por separado.
src/app/
  core/       interceptors/ · guards/ · services/ · layout/
  shared/     ui/ · pipes/ · directives/ · validators/
  features/
    pedidos/  pedidos.routes.ts · pages/ · components/ · data/
    pagos/    pagos.routes.ts · ...
  app.config.ts · app.routes.ts
Si repreguntan

¿Y con standalone en Angular 20? — Misma idea, otra implementación: Core son providers en app.config.ts; Shared son componentes standalone que se importan donde se usan; cada feature es un archivo de rutas con sus propios providers. El backoffice ya es standalone; el punto de venta sigue con NgModules, que siguen soportados.

¿Cómo evitas que un feature importe de otro? — Regla de lint de fronteras y alias como @shared. Si dos features necesitan lo mismo, se sube a Shared: así nació la librería común del backoffice.

¿Dónde pones el estado? — En el componente si solo él lo usa; en un servicio provisto en la ruta del feature si lo comparte el feature; en root si es global, como la sesión.

¿Por qué microfrontends? — Eran 16 áreas con ritmos de entrega distintos; así cada una se construye y se despliega sola. El costo es compartir dependencias sin duplicar Angular. Para un equipo chico, un monolito modular es mejor.

2 · Signals

¿Qué es un signal y por qué lo usas?

Respuesta Un signal es un valor reactivo: cuando cambia, Angular actualiza solo lo que depende de él, y un computed deriva valores de otros signals. En recepción electrónica, al capturar los lotes de la mercancía recibida, los lotes son un signal y un computed habilita "Agregar lote" solo si hay lote, piezas y caducidad, y lo bloquea al completar las piezas. La regla queda en un solo lugar y sin suscripciones.
Si repreguntan

¿Cuándo usas effect? — Solo para efectos hacia afuera de Angular: storage, logs, una librería externa. Si es calcular un valor a partir de otro, uso computed.

¿Cómo actualizas un arreglo? — Inmutable: update(xs => [...xs, nuevo]). Si haces push, la referencia no cambia y no notifica.

¿Signals reemplaza a RxJS? — No. Signals para estado; RxJS para eventos en el tiempo. Se conectan con toSignal y toObservable.

¿linkedSignal? — Un signal escribible que se reinicia cuando cambia su fuente, como la página que vuelve a 1 al cambiar el filtro.

¿Zoneless?provideZonelessChangeDetection(), estable desde Angular 20.2: la detección de cambios la disparan signals y eventos, sin Zone.js.

¿Cómo comunicas componentes padre e hijo?

Respuesta El padre pasa datos con input() y el hijo avisa con output(). Así funciona el buscador de productos que creé para la librería del backoffice: compra directa y devoluciones a proveedor le pasan la etiqueta por input, y cuando el usuario elige un producto lo reciben por output para agregarlo como renglón. El buscador no sabe si es compra o devolución; por eso sirve en las dos.

3 · RxJS

¿Observable o Promesa?

Respuesta Una promesa da un solo valor y no se cancela; un Observable da varios valores en el tiempo y sí se cancela. Por eso en el backoffice "Guardar pedido" es una promesa con await — es una sola llamada —, mientras que el buscador, que escucha cada tecla del usuario, es RxJS.

¿switchMap, mergeMap, concatMap o exhaustMap?

Respuesta Los cuatro deciden qué pasa cuando llega un valor nuevo: switchMap cancela el anterior, concatMap hace cola, mergeMap va en paralelo y exhaustMap ignora lo nuevo hasta terminar. En el punto de venta el doble clic en "Cobrar" mandaba el cobro dos veces; lo resolví con una directiva que ignora clics repetidos, hoy en 23 botones. Ahora la haría con exhaustMap, que ignora los clics mientras el cobro siga en proceso.
Para recordarlo: ¿buscar? switch · ¿en orden? concat · ¿todos a la vez? merge · ¿uno a la vez, ignorando el resto? exhaust.

¿Cómo haces varias peticiones a la vez?

Respuesta Con forkJoin: lanza varias peticiones en paralelo y responde una sola vez, cuando todas terminan. En el punto de venta, al agregar un producto a una venta de subrogación se consultan juntas siete cosas — existencia, precio, alertas, lealtad, promociones — y el producto solo se agrega si todas responden bien. El cajero espera lo que tarda la más lenta, no la suma.

¿Cómo evitas fugas de memoria?

Respuesta Cerrando toda suscripción manual, con async pipe, toSignal o takeUntilDestroyed(). Mi buscador vive en pantallas que se abren y cierran todo el día; si su suscripción al teclado quedara viva, seguiría buscando con la pantalla cerrada, así que la cierro con takeUntil al destruirse. Hoy usaría takeUntilDestroyed().
Si repreguntan

¿Dónde va el catchError? — Dentro del switchMap: un error completa el observable; adentro solo muere esa petición.

¿Subject o BehaviorSubject? — Subject no guarda valor (eventos); BehaviorSubject guarda el último (estado). En el punto de venta hay servicios con BehaviorSubject para compartir estado; en código nuevo uso signals.

¿Cold vs hot? — Cold: cada suscriptor ejecuta la fuente, como HTTP. Hot: se comparte, como un evento. Para compartir una petición, shareReplay.

4 · Reactive Forms

¿Por qué Reactive Forms?

Respuesta Porque el formulario vive en TypeScript: es tipado y puedo cambiar sus reglas desde el código. El cobro del punto de venta es así: forma de pago, importes y datos de tarjeta, con validaciones que se prenden o apagan según lo que elige el cajero, sin tocar la plantilla.

¿Cómo haces un validador personalizado?

Respuesta Es una función que recibe el control y devuelve null si es válido o un objeto con el error. En el punto de venta, al abrir caja, el validador shouldBeLess revisa que el fondo inicial no pase del máximo autorizado para la sucursal; si lo pasa, muestra el aviso y no deja abrir la caja.

¿Cómo cambias las validaciones según lo que elige el usuario?

Respuesta Con setValidators o clearValidators y después updateValueAndValidity(). En el cobro con tarjeta, si el cajero marca Amex aparece un campo de 4 dígitos obligatorio; si lo desmarca, el campo se deshabilita y deja de validarse, para que la regla de Amex no bloquee otras tarjetas.

¿Cómo manejas una lista dinámica de campos?

Respuesta Con FormArray, una lista de controles a la que agregas o quitas renglones. En el alta de clientes del punto de venta cada teléfono es un renglón que el cajero agrega o quita, y en el programa de apego al tratamiento cada medicamento es un renglón con dosis, frecuencia, días y fechas.
Si repreguntan

¿value o getRawValue?value excluye deshabilitados; getRawValue() trae todo.

¿setValue o patchValue?setValue exige todos los campos; patchValue acepta una parte. Para cargar datos a editar, patchValue, que usamos mucho en los dos proyectos.

¿Errores al enviar? — Si es inválido, markAllAsTouched() y los mensajes aparecen porque dependen de touched.

5 · Componentes reutilizables y sistema de UI

¿Qué hace reutilizable a un componente?

Respuesta Una API chica con input y output, sin lógica de negocio adentro, que esconda lo complejo. El buscador que creé se usa en compra directa y en devoluciones: resuelve en un solo lugar esperar a que el usuario deje de teclear, pedir un mínimo de caracteres y cargar más resultados al bajar, y cada pantalla solo recibe el producto elegido.

¿Qué es ControlValueAccessor?

Respuesta · sin caso en producción Es la interfaz que hace que tu componente funcione con formControlName como un input normal. No lo he necesitado en producción, pero es la mejora que le haría a mi buscador: hoy cada pantalla copia a mano el producto elegido a su formulario, y con ControlValueAccessor sería un campo más, con obligatorio y deshabilitado incluidos.

¿Cómo mantienes la consistencia visual?

Respuesta Con componentes base compartidos y tokens de diseño en lugar de estilos sueltos. En el punto de venta todas las pantallas usan los mismos botones de pago y de pie de pantalla; cuando apareció el doble cobro, puse la protección en esos botones y llegó a toda la aplicación con un solo cambio.

6 · APIs REST e interceptores

¿Qué es un interceptor y para qué lo usas?

Respuesta Es una función que pasa por cada petición HTTP para resolver lo que es común a todas. En el punto de venta uno agrega el token de sesión, otro corta las peticiones que tardan demasiado y otro avisa al cajero cuando la venta perdió su contexto. Yo creé además un ErrorHandler global que manda cada error no controlado a un log con sucursal y caja, para diagnosticar sin llamar a la tienda.
Si repreguntan

¿Interceptor o ErrorHandler? — El interceptor ve errores HTTP y reacciona por petición: redirigir en 401, reintentar. El ErrorHandler ve todo lo no controlado — errores de JavaScript, módulos lazy que no cargaron después de un deploy — y lo registra.

¿Cómo agregas el token? — La petición es inmutable: next(req.clone({ setHeaders: { Authorization: 'Bearer ' + token } })).

¿Loading global? — Con contador, no booleano, y se baja en finalize, que corre en éxito, error y cancelación.

¿Refresh token? — En el 401, un solo refresh compartido y reintento de la petición original con switchMap; si falla, cerrar sesión.

7 · Rendimiento — OnPush, lazy loading, trackBy

¿Cómo funciona OnPush?

Respuesta · sin caso en producción Con OnPush, Angular revisa el componente solo si cambia la referencia de un input, hay un evento en él o cambia un signal que lee; por eso va con datos inmutables. En nuestros proyectos se usa la detección por defecto, y en el backoffice llevamos el estado nuevo a signals, que ya actualizan solo lo que cambia. OnPush lo pondría en componentes que solo muestran datos, como las tablas de los monitores.

¿Para qué sirve trackBy o track?

Respuesta Identifica cada elemento de una lista para que Angular no redibuje todo cuando cambian los datos. En el monitor de pedidos del backoffice cada consulta reemplaza la lista de la tabla, y con trackBy por folio Angular reutiliza las filas que no cambiaron.

¿Lazy loading o @defer?

Respuesta Lazy loading descarga el código de una ruta solo cuando entras a ella; @defer hace lo mismo con partes de una pantalla. En el backoffice cada área — compra directa, devoluciones, recepción — es un microfrontend que se descarga al entrar a su menú: quien trabaja en devoluciones nunca descarga el código de recepción.

Una pantalla va lenta, ¿qué haces?

Respuesta Primero mido, con Angular DevTools y la pestaña Network, y después corrijo lo que salga. En el buscador de productos, para no saturar al servidor, espera medio segundo sin teclear, pide al menos 3 caracteres y trae los resultados por páginas al bajar, en vez de miles de golpe.

8 · Testing

¿Haces pruebas unitarias?

Respuesta Sí: pruebo cada unidad aislada, simulando sus dependencias. En pedido manual probé el servicio que guarda un pedido en sus tres caminos — sale bien, el servidor responde un error de negocio o falla la llamada — con Jasmine simulando la capa HTTP, y verifiqué que en error se registra y se avisa al usuario. SonarQube medía la cobertura en cada Pull Request.

9 · Java 20 y Quarkus (deseable)

Tu proyecto propio: "Farmacia API" — ya está en Desktop\farmacia-api, funcionando y con 6/6 pruebas. Antes de usar estas respuestas, hazlo tuyo (30 min): córrelo con correr.bat, prueba los endpoints en Swagger, lee los archivos en el orden del LEEME.md y haz el cambio que ahí se indica. Los ejercicios J1–J6 explican cada parte paso a paso.

La frase de entrada, siempre la misma: "En producción no he usado Java; mi backend en el trabajo fue .NET. Pero como la vacante lo pedía, hice un proyecto propio con Quarkus para conocerlo."

Si no alcanzas a correr y leer tu proyecto, usa esta respuesta, que es verdad hoy:

"En producción no he usado Java; mi backend fue .NET, con APIs REST en AWS Lambda. Conozco los conceptos de Quarkus porque son los mismos que en .NET — endpoints anotados, inyección de dependencias, un ORM, validación del lado del servidor — y ya estoy armando una API propia con Quarkus para practicarlo. Desde el front puedo trabajar con su API desde el primer día, por contrato, con el OpenAPI que publica."

Úsala solo si no alcanzas a correr y leer tu proyecto. Si ya lo hiciste, las respuestas de abajo, que hablan de tu API, son verdad.

¿Has trabajado con Java o Quarkus?

Respuesta Quarkus es un framework Java para APIs REST, rápido y pensado para contenedores. En producción no lo he usado — mi backend fue .NET —, pero hice una API de práctica con él para el inventario de una farmacia: buscar productos, darlos de alta y descontar existencia al vender, con validaciones, manejo de errores y pruebas, lista para conectarse con Angular.

¿Cómo haces un endpoint REST en Quarkus?

Respuesta Con anotaciones: @Path en la clase y @GET o @POST en los métodos, igual que un controller de .NET. En mi API, GET /productos?q= busca por nombre, GET /productos/{id} devuelve uno o 404, y POST da de alta y responde 201 con la ruta del nuevo.
@Path("/productos")
public class ProductoResource {
  @Inject InventarioService inventario;

  @GET
  public List<ProductoDto> buscar(@QueryParam("q") @DefaultValue("") String q) {
    return inventario.buscar(q);
  }

  @GET @Path("/{id}")
  public ProductoDto obtener(@PathParam("id") Long id) {
    return inventario.obtener(id);               // lanza NotFoundException → 404
  }

  @POST
  public Response crear(@Valid ProductoInput input) {
    ProductoDto p = inventario.crear(input);
    return Response.created(URI.create("/productos/" + p.id())).entity(p).build();
  }
}

¿Cómo funciona la inyección de dependencias en Quarkus?

Respuesta Con CDI: la clase se marca @ApplicationScoped y se pide con @Inject. En mi API el endpoint solo recibe y responde, y la regla "no vender más de lo que hay" vive en un InventarioService inyectado; así la regla se prueba sola.

¿Cómo accedes a la base de datos?

Respuesta Con Hibernate Panache: la entidad ya trae persist, findById y list, y las escrituras van en @Transactional. En mi API, descontar existencia es transaccional: si no alcanza, se lanza el error y no queda nada guardado a medias.
@Entity
public class Producto extends PanacheEntity {
  public String sku;
  public String nombre;
  public BigDecimal precio;
  public int existencia;
}

¿Cómo validas lo que llega a la API?

Respuesta Con anotaciones como @NotBlank o @Positive y @Valid en el endpoint. Si doy de alta un producto sin nombre o con precio negativo, mi API responde 400 con el campo que falló, sin escribir un solo if.
public record ProductoInput(
  @NotBlank String sku,
  @NotBlank String nombre,
  @Positive BigDecimal precio,
  @PositiveOrZero int existencia) {}

¿Cómo manejas los errores en la API?

Respuesta Con excepciones propias que un mapeador convierte al código HTTP correcto. En mi API, vender más de lo que hay lanza StockInsuficienteException y responde 409 con un mensaje como "hay 12, pides 999", que el front muestra tal cual.
@ServerExceptionMapper
public RestResponse<Map<String, String>> stock(StockInsuficienteException e) {
  return RestResponse.status(Response.Status.CONFLICT, Map.of("error", e.getMessage()));
}

¿Qué usas de Java moderno?

Respuesta Records para los datos, streams para transformar listas y Optional para evitar nulls. Mi API devuelve un record con el campo calculado "agotado", convierte listas con stream().map().toList() — como un Select de LINQ — y usa Optional para responder 404 cuando algo no existe.
public record ProductoDto(Long id, String sku, String nombre, BigDecimal precio, boolean agotado) {
  static ProductoDto from(Producto p) {
    return new ProductoDto(p.id, p.sku, p.nombre, p.precio, p.existencia == 0);
  }
}

¿Cómo pruebas una API en Quarkus?

Respuesta Con @QuarkusTest, que levanta la app, y RestAssured, que hace la petición y revisa la respuesta. Mi API tiene seis pruebas: la búsqueda, el 404, el alta con 201, el 400 por precio negativo, el 409 por vender de más y el permiso de CORS para Angular.
@QuarkusTest
class ProductoResourceTest {
  @Test
  void idInexistenteDevuelve404() {
    given().when().get("/productos/9999").then().statusCode(404);
  }
}

¿Cómo conectas un front Angular con un backend Quarkus?

Respuesta Por el contrato OpenAPI y habilitando CORS para el origen del front. En mi API habilité CORS solo para localhost:4200 y lo verifiqué con una prueba; las interfaces TypeScript salen del Swagger que publica Quarkus.
# application.properties
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:farmacia
quarkus.hibernate-orm.database.generation=drop-and-create
%dev.quarkus.http.cors.enabled=true
%dev.quarkus.http.cors.origins=http://localhost:4200

¿Qué diferencia hay entre Java 20 y 21?

Respuesta Java 21 es LTS, con soporte largo; la 20 no, y lo que en la 20 era preview, como los virtual threads, en la 21 es estable. Como me dijeron que usan Java 20, en mi API usé solo lo estable ahí: records, streams y Optional.

Pregunta que puedes hacer tú: "¿Tienen planeado pasar a Java 21, que es la LTS?"

Tabla de equivalencias .NET → Java / Quarkus
.NETJava / Quarkus
[ApiController] + [Route]@Path en la clase
[HttpGet("{id}")]@GET @Path("/{id}") + @PathParam
[FromQuery] · [FromBody]@QueryParam · parámetro sin anotación
AddScoped / AddSingleton@RequestScoped / @ApplicationScoped
Entity Framework · SaveChangesHibernate Panache · persist() + @Transactional
Data Annotations [Required]Bean Validation @NotBlank
Middleware de excepciones@ServerExceptionMapper
LINQ Select / WhereStreams map / filter
appsettings.{Env}.jsonapplication.properties con %dev. / %prod.
xUnit + WebApplicationFactoryJUnit 5 + @QuarkusTest + RestAssured
Swashbuckle / SwaggerSmallRye OpenAPI (/q/swagger-ui)
dotnet watch./mvnw quarkus:dev
NuGet · .csprojMaven · pom.xml

10 · Comandos básicos (Angular CLI y npm)

¿Qué comandos del CLI usas en el día a día?

Respuesta ng serve para desarrollar, ng generate para crear piezas, ng build por ambiente, ng test y ng update. El punto de venta se compila para desarrollo, QA y producción, cada uno con su servidor, y lo actualicé de Angular 17 a 19 con ng update, una versión a la vez; la segunda tocó 204 archivos.
ComandoPara qué
ng new mi-app --style=scss --routingCrear proyecto
ng serve -oServidor de desarrollo y abrir el navegador
ng g c features/pedidos/listaComponente (standalone por default)
ng g s core/services/sesionServicio
ng g guard core/guards/auth · ng g interceptor core/interceptors/authGuard e interceptor (funcionales)
ng g pipe · ng g directive · ng g library uiPipe, directiva, librería
ng build --configuration productionCompilar para un ambiente
ng test --code-coveragePruebas con reporte de cobertura
ng add @angular/materialInstalar y configurar una librería
ng update @angular/core @angular/cliActualizar de versión con migraciones automáticas
npm install · npm ciInstalar dependencias · instalar exacto desde el lockfile (en pipelines)
Si repreguntan

¿npm install o npm ci?ci instala exactamente lo del package-lock.json y borra node_modules antes: es reproducible, por eso va en pipelines.

¿Cómo manejas ambientes? — Configuraciones en angular.json con fileReplacements de environment.ts, y ng build --configuration qa.

¿Qué haces si ng update marca problemas? — Una versión mayor a la vez, reviso la guía oficial (update.angular.dev), dejo que corran las migraciones y compilo y pruebo antes de subir.

11 · Git

¿Cómo es tu flujo de trabajo con Git?

Respuesta Una rama por tarea, commits chicos y todo entra por Pull Request con revisión. En Azure DevOps cada tarea tenía su rama desde DEV, como DEV_18252_BuscarFacturasElectronicas; antes del PR traía DEV a mi rama, resolvía los conflictos ahí, y el PR pasaba revisión y SonarQube.
git switch DEV && git pull                     # base al día
git switch -c DEV_18252_BuscarFacturas         # rama de la tarea
git add . && git commit -m "feat: buscar facturas por folio"
git fetch origin && git merge origin/DEV       # traer lo nuevo de DEV y resolver conflictos
git push -u origin DEV_18252_BuscarFacturas    # y se abre el Pull Request
ComandoPara qué
git status · git log --oneline --graphVer estado e historial
git switch -c ramaCrear y cambiar de rama
git pull = fetch + mergeTraer y combinar cambios del remoto
git stash · git stash popGuardar cambios a medias para cambiar de rama
git cherry-pick <hash>Llevar un commit puntual a otra rama (un fix urgente)
git revert <hash>Deshacer un commit ya compartido creando otro commit
git reset --soft HEAD~1Deshacer el último commit local sin perder los cambios
git diff · git blame archivoVer diferencias · quién cambió cada línea
Si repreguntan

¿Merge o rebase?merge une las historias y conserva todo, con un commit de merge; es seguro en ramas compartidas, y es lo que usábamos para traer DEV. rebase reescribe tus commits encima de la base y deja la historia lineal; solo en una rama local que nadie más bajó.

¿Cómo resuelves un conflicto? — Traigo la base a mi rama, abro los archivos marcados, decido línea por línea — con quien hizo el otro cambio si hace falta —, compilo y corro las pruebas antes de hacer commit del merge.

¿revert o reset?revert para algo que ya está en el remoto, porque no reescribe historia. reset solo para commits locales que nadie más tiene.

¿Qué revisas en un Pull Request? — Que haga lo que pide la tarea, que no rompa otra cosa, nombres claros, que se manejen el error y el estado de carga, y que pasen las pruebas y Sonar.

¿Convención de commits? — Prefijos como feat:, fix:, refactor:, chore: (Conventional Commits) y el número del work item para ligarlo en Azure DevOps.

Ejercicios

Veintiún ejercicios cortos, de 5 a 15 minutos: quince de Angular, uno por requisito de la vacante, y seis de Java con Quarkus que forman tu proyecto propio. Reglas: tecléalos de cero; abre la pista solo si llevas 5 minutos atorado y la solución solo al terminar. Márcalo como hecho cuando te salga sin mirar.

Hechos sin mirar: 0 / 15

Preparación (una sola vez, 5 min)

ng new practica --style=scss --ssr=false --routing
cd practica
ng serve

Para los ejercicios que necesitan datos, crea este servicio falso (simula la red con delay). Así no dependes de ningún backend:

// src/app/fake-api.ts
import { Injectable } from '@angular/core';
import { Observable, of, delay, throwError } from 'rxjs';

export interface Producto { sku: string; nombre: string; precio: number; existencia: number; }
const PRODUCTOS: Producto[] = [
  { sku: 'A1', nombre: 'Paracetamol 500mg', precio: 35, existencia: 12 },
  { sku: 'A2', nombre: 'Paracetamol infantil', precio: 48, existencia: 0 },
  { sku: 'B1', nombre: 'Ibuprofeno 400mg', precio: 52, existencia: 8 },
  { sku: 'C1', nombre: 'Omeprazol 20mg', precio: 89, existencia: 5 },
];

@Injectable({ providedIn: 'root' })
export class FakeApi {
  buscar(q: string): Observable<Producto[]> {
    const r = PRODUCTOS.filter(p => p.nombre.toLowerCase().includes(q.toLowerCase()));
    return of(r).pipe(delay(300 + Math.random() * 700));      // latencia variable
  }
  guardar(): Observable<{ ok: true }> { return of({ ok: true as const }).pipe(delay(1500)); }
  categorias() { return of(['Analgésicos', 'Gástricos']).pipe(delay(400)); }
  sucursales() { return of(['Centro', 'Norte']).pipe(delay(800)); }
  fallar() { return throwError(() => new Error('500')); }
}

Nivel 1 · Signals

D1 · Contador con derivados 5 min

Requisito: Signals para estado en componentes

Un contador con botones +, y Reiniciar. Muestra también el doble del valor y el texto "par/impar". El botón − se deshabilita en 0.

Hecho cuando: usas signal, dos computed, y update en vez de leer y volver a asignar.

Solución
@Component({
  selector: 'app-contador',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <p>{{ n() }} · doble: {{ doble() }} · {{ paridad() }}</p>
    <button (click)="n.update(v => v - 1)" [disabled]="n() === 0">−</button>
    <button (click)="n.update(v => v + 1)">+</button>
    <button (click)="n.set(0)">Reiniciar</button>`
})
export class Contador {
  n = signal(0);
  doble = computed(() => this.n() * 2);
  paridad = computed(() => this.n() % 2 === 0 ? 'par' : 'impar');
}

D2 · Carrito con total 8 min

Requisito: Signals + inmutabilidad + @for con track

Lista de productos (usa el arreglo del FakeApi) con botón "Agregar". Un carrito muestra las líneas, la cantidad por producto, un botón "Quitar" y el total. El botón "Cobrar" solo se habilita si hay algo en el carrito.

Hecho cuando: el carrito es signal<Linea[]>, lo modificas con update creando arreglos nuevos (sin push), el total es computed y el @for tiene track.

Pista

Si el producto ya está, usa map para devolver una línea nueva con cantidad + 1; si no, [...ls, { ...p, cantidad: 1 }].

Solución
type Linea = Producto & { cantidad: number };

export class Carrito {
  lineas = signal<Linea[]>([]);
  total = computed(() => this.lineas().reduce((s, l) => s + l.precio * l.cantidad, 0));
  puedeCobrar = computed(() => this.lineas().length > 0);

  agregar(p: Producto) {
    this.lineas.update(ls => ls.some(l => l.sku === p.sku)
      ? ls.map(l => l.sku === p.sku ? { ...l, cantidad: l.cantidad + 1 } : l)
      : [...ls, { ...p, cantidad: 1 }]);
  }
  quitar(sku: string) { this.lineas.update(ls => ls.filter(l => l.sku !== sku)); }
}
// plantilla
@for (l of lineas(); track l.sku) {
  <li>{{ l.nombre }} × {{ l.cantidad }} <button (click)="quitar(l.sku)">Quitar</button></li>
} @empty { <li>Carrito vacío</li> }
<p>Total: {{ total() | currency:'MXN' }}</p>
<button [disabled]="!puedeCobrar()">Cobrar</button>

Nivel 2 · Componentes reutilizables

D3 · Tarjeta de producto (padre-hijo) 8 min

Requisito: componentes desacoplados · input/output · OnPush

Separa D2 en dos: app-producto-card recibe un producto, muestra nombre y precio, dice "Agotado" si la existencia es 0 y emite agregar. El padre pinta las tarjetas y lleva el carrito.

Hecho cuando: la tarjeta usa input.required, output, un computed para "agotado", es OnPush y no inyecta ningún servicio.

Solución
@Component({
  selector: 'app-producto-card',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [CurrencyPipe],
  template: `
    <h4>{{ producto().nombre }}</h4>
    <p>{{ producto().precio | currency:'MXN' }}</p>
    @if (agotado()) { <span>Agotado</span> }
    <button (click)="agregar.emit(producto())" [disabled]="agotado()">Agregar</button>`
})
export class ProductoCard {
  producto = input.required<Producto>();
  agregar = output<Producto>();
  agotado = computed(() => this.producto().existencia === 0);
}
// padre
@for (p of productos; track p.sku) {
  <app-producto-card [producto]="p" (agregar)="carrito.agregar($event)" />
}

D4 · Tarjeta genérica con ng-content 6 min

Requisito: componentes reutilizables · consistencia de diseño

Crea ui-card con tres zonas: título, cuerpo y acciones. Quien la usa decide qué va en cada una. Los colores y el espaciado salen de variables CSS.

Hecho cuando: usas <ng-content select="..."> para título y acciones, un ng-content sin select para el cuerpo, y el SCSS solo tiene var(--...).

Solución
// ui-card.ts — template
<header><ng-content select="[card-title]" /></header>
<section><ng-content /></section>
<footer><ng-content select="[card-actions]" /></footer>

// ui-card.scss
:host { display: block; background: var(--surface); border-radius: var(--radius); padding: var(--space-4); }
footer { display: flex; gap: var(--space-2); justify-content: flex-end; }

// styles.scss
:root { --surface: #fff; --radius: 8px; --space-2: 8px; --space-4: 16px; }

// uso
<ui-card>
  <h3 card-title>Pedido 1024</h3>
  <p>3 productos · $250</p>
  <button card-actions>Ver</button>
</ui-card>

D5 · Selector de cantidad que funciona con formularios 12 min

Requisito: componentes reutilizables · Reactive Forms

Un componente − 3 + con mínimo configurable, que se pueda usar como <ui-cantidad formControlName="cantidad" [min]="1" /> y respete cuando el form lo deshabilita.

Hecho cuando: implementas los 4 métodos de ControlValueAccessor y registras NG_VALUE_ACCESSOR con multi: true. Pruébalo con un FormControl y muestra su valor debajo.

Pista

Los 4 métodos: entra (writeValue), sale (registerOnChange), tocado (registerOnTouched), deshabilitar (setDisabledState).

Solución
@Component({
  selector: 'ui-cantidad',
  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiCantidad), multi: true }],
  template: `
    <button type="button" (click)="cambiar(-1)" [disabled]="disabled()">−</button>
    <span>{{ valor() }}</span>
    <button type="button" (click)="cambiar(1)" [disabled]="disabled()">+</button>`
})
export class UiCantidad implements ControlValueAccessor {
  min = input(0);
  valor = signal(0);
  disabled = signal(false);
  private onChange = (v: number) => {};
  private onTouched = () => {};

  writeValue(v: number | null) { this.valor.set(v ?? 0); }
  registerOnChange(fn: (v: number) => void) { this.onChange = fn; }
  registerOnTouched(fn: () => void) { this.onTouched = fn; }
  setDisabledState(d: boolean) { this.disabled.set(d); }

  cambiar(delta: number) {
    const v = Math.max(this.min(), this.valor() + delta);
    this.valor.set(v); this.onChange(v); this.onTouched();
  }
}

Nivel 3 · RxJS

D6 · Buscador de productos 12 min

Requisito: RxJS para flujos asíncronos · Signals

Un input que busca con FakeApi.buscar mientras escribes. Espera 300 ms sin teclear, busca a partir de 2 letras, no repite la misma búsqueda, cancela la anterior si escribes de nuevo, y muestra "Buscando…" y "Sin resultados".

Hecho cuando: usas valueChangesdebounceTimedistinctUntilChangedswitchMap y toSignal. Prueba: escribe "para" rápido y borra una letra: solo debe quedar el último resultado.

Pista

El estado de carga: tap antes de la petición y finalize dentro del switchMap.

Solución
@Component({
  selector: 'app-buscador',
  imports: [ReactiveFormsModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <input [formControl]="q" placeholder="Buscar producto" />
    @if (cargando()) { <p>Buscando…</p> }
    <ul>
      @for (p of resultados(); track p.sku) { <li>{{ p.nombre }}</li> }
      @empty { @if (q.value.length >= 2 && !cargando()) { <li>Sin resultados</li> } }
    </ul>`
})
export class Buscador {
  private api = inject(FakeApi);
  q = new FormControl('', { nonNullable: true });
  cargando = signal(false);

  resultados = toSignal(this.q.valueChanges.pipe(
    map(v => v.trim()),
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(q => {
      if (q.length < 2) return of([]);
      this.cargando.set(true);
      return this.api.buscar(q).pipe(
        catchError(() => of([])),
        finalize(() => this.cargando.set(false))
      );
    })
  ), { initialValue: [] as Producto[] });
}

D7 · Botón "Cobrar" sin doble envío 8 min

Requisito: RxJS para manejo de eventos

Un botón que llama a FakeApi.guardar() (tarda 1.5 s). Aunque le des 5 clics rápidos, solo debe hacer una petición mientras la anterior no termine. Cuenta en pantalla cuántas veces se guardó.

Hecho cuando: usas un Subject para los clics + exhaustMap + takeUntilDestroyed. Experimento: cámbialo por mergeMap y por concatMap y observa el contador: así se te queda la diferencia.

Solución
export class Cobrar {
  private api = inject(FakeApi);
  private clics = new Subject<void>();
  guardados = signal(0);
  guardando = signal(false);

  constructor() {
    this.clics.pipe(
      exhaustMap(() => {
        this.guardando.set(true);
        return this.api.guardar().pipe(finalize(() => this.guardando.set(false)));
      }),
      takeUntilDestroyed()
    ).subscribe(() => this.guardados.update(n => n + 1));
  }
  cobrar() { this.clics.next(); }
}
// <button (click)="cobrar()">{{ guardando() ? 'Guardando…' : 'Cobrar' }}</button> Guardados: {{ guardados() }}

Resultado del experimento: exhaustMap → 1 · mergeMap → 5 casi a la vez · concatMap → 5, una tras otra (7.5 s).

D8 · Cargar dos catálogos a la vez 6 min

Requisito: RxJS para flujos asíncronos

Al abrir la pantalla, carga categorias() y sucursales() en paralelo y muestra todo cuando ambos terminaron. Si uno falla, muestra un mensaje de error.

Hecho cuando: usas forkJoin con un objeto, toSignal, y un catchError. Extra: cambia sucursales() por fallar() y verifica el mensaje.

Solución
export class Catalogos {
  private api = inject(FakeApi);
  error = signal(false);
  datos = toSignal(
    forkJoin({ categorias: this.api.categorias(), sucursales: this.api.sucursales() }).pipe(
      catchError(() => { this.error.set(true); return of(null); })
    )
  );
}
// @if (error()) { <p>No se pudo cargar</p> }
// @else if (datos(); as d) { {{ d.categorias.length }} categorías · {{ d.sucursales.length }} sucursales }
// @else { <p>Cargando…</p> }

Nivel 4 · Reactive Forms

D9 · Login con validaciones 8 min

Requisito: Reactive Forms

Formulario con correo (requerido, formato email) y contraseña (requerida, mínimo 8). Los errores aparecen solo si el campo fue tocado. Al enviar inválido, se muestran todos los errores.

Hecho cuando: usas NonNullableFormBuilder, hasError con touched, markAllAsTouched() y getRawValue().

Solución
export class Login {
  private fb = inject(NonNullableFormBuilder);
  form = this.fb.group({
    correo: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]],
  });
  enviar() {
    if (this.form.invalid) { this.form.markAllAsTouched(); return; }
    console.log(this.form.getRawValue());
  }
}
// <form [formGroup]="form" (ngSubmit)="enviar()">
//   <input formControlName="correo" />
//   @if (form.controls.correo.touched && form.controls.correo.hasError('email')) { <small>Correo inválido</small> }
//   <input type="password" formControlName="password" />
//   @if (form.controls.password.touched && form.controls.password.hasError('minlength')) { <small>Mínimo 8</small> }
//   <button type="submit">Entrar</button>
// </form>

D10 · Validador de RFC 6 min

Requisito: Reactive Forms · validadores reutilizables

Un ValidatorFn reutilizable que marque error rfc si el texto no tiene el formato: 3 o 4 letras, 6 dígitos, 3 alfanuméricos. Vacío es válido (de eso se encarga required).

Solución
export function rfcValidator(): ValidatorFn {
  const re = /^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/;
  return (c: AbstractControl): ValidationErrors | null =>
    !c.value || re.test(String(c.value).toUpperCase()) ? null : { rfc: true };
}
// rfc: ['', [Validators.required, rfcValidator()]]

D11 · Pedido con líneas dinámicas 12 min

Requisito: Reactive Forms · FormArray

Formulario de pedido con proveedor y una lista de líneas (SKU requerido, cantidad mínima 1). Botones "Agregar línea" y "Quitar". No se puede guardar sin al menos una línea.

Hecho cuando: usas fb.array con Validators.minLength(1), formArrayName, [formGroupName]="i", push y removeAt.

Solución
export class Pedido {
  private fb = inject(NonNullableFormBuilder);
  form = this.fb.group({
    proveedor: ['', Validators.required],
    lineas: this.fb.array([this.nuevaLinea()], Validators.minLength(1)),
  });
  get lineas() { return this.form.controls.lineas; }
  nuevaLinea() {
    return this.fb.group({ sku: ['', Validators.required], cantidad: [1, [Validators.required, Validators.min(1)]] });
  }
  agregar() { this.lineas.push(this.nuevaLinea()); }
  quitar(i: number) { this.lineas.removeAt(i); }
}
// <div formArrayName="lineas">
//   @for (l of lineas.controls; track l; let i = $index) {
//     <div [formGroupName]="i">
//       <input formControlName="sku" /> <input type="number" formControlName="cantidad" />
//       <button type="button" (click)="quitar(i)">Quitar</button>
//     </div>
//   }
// </div>
// <button type="button" (click)="agregar()">Agregar línea</button>
// <button [disabled]="form.invalid">Guardar</button>

D12 · Validador que compara dos fechas 6 min

Requisito: Reactive Forms · validación cruzada

En un grupo con desde y hasta, error rango si "desde" es mayor que "hasta". El mensaje aparece debajo del grupo.

Solución
export function rangoFechas(a: string, b: string): ValidatorFn {
  return (g: AbstractControl) => {
    const desde = g.get(a)?.value, hasta = g.get(b)?.value;
    return desde && hasta && desde > hasta ? { rango: true } : null;
  };
}
// form = this.fb.group({ desde: [''], hasta: [''] }, { validators: rangoFechas('desde', 'hasta') });
// @if (form.hasError('rango')) { <small>"Desde" debe ser antes de "hasta"</small> }

Nivel 5 · HTTP, arquitectura y rendimiento

D13 · Interceptores de token y de loading 10 min

Requisito: interceptores HTTP

Dos interceptores funcionales: uno agrega Authorization: Bearer ... (toma el token de localStorage), otro lleva un contador de peticiones activas en un servicio con signal. Regístralos en app.config.ts. Verifica en la pestaña Network la cabecera con cualquier petición (por ejemplo, a /assets).

Solución
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = localStorage.getItem('token');
  return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req);
};

@Injectable({ providedIn: 'root' })
export class Loading { activas = signal(0); visible = computed(() => this.activas() > 0); }

export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
  const l = inject(Loading);
  l.activas.update(n => n + 1);
  return next(req).pipe(finalize(() => l.activas.update(n => n - 1)));
};

// app.config.ts
provideHttpClient(withInterceptors([authInterceptor, loadingInterceptor]))

D14 · Core, Shared y dos features lazy 10 min

Requisito: arquitectura modular · lazy loading

Organiza el proyecto: core/ (los interceptores de D13), shared/ (ui-card y el validador RFC), y features/productos y features/pedidos, cada uno con su archivo de rutas cargado con loadChildren. Mete ahí los ejercicios anteriores.

Hecho cuando: en la pestaña Network ves que el chunk de "pedidos" se descarga solo al navegar a /pedidos, y ningún feature importa de otro.

Solución
// app.routes.ts
export const routes: Routes = [
  { path: 'productos', loadChildren: () => import('./features/productos/productos.routes').then(m => m.PRODUCTOS_ROUTES) },
  { path: 'pedidos', loadChildren: () => import('./features/pedidos/pedidos.routes').then(m => m.PEDIDOS_ROUTES) },
  { path: '', redirectTo: 'productos', pathMatch: 'full' },
];

// features/pedidos/pedidos.routes.ts
export const PEDIDOS_ROUTES: Routes = [
  { path: '', component: Pedido },
];

D15 · Ver OnPush con tus propios ojos 8 min

Requisito: optimización de rendimiento (OnPush)

Un hijo OnPush que muestra producto.precio (recibido como @Input() clásico, no signal). En el padre, dos botones: "Subir precio (mutando)" hace this.producto.precio++; "Subir precio (inmutable)" hace this.producto = { ...this.producto, precio: this.producto.precio + 1 }.

Qué debes observar: con el primero el hijo no se actualiza; con el segundo sí. Explica en voz alta por qué. Ese es exactamente el ejemplo que puedes contar en la entrevista.

Solución
@Component({
  selector: 'app-precio', changeDetection: ChangeDetectionStrategy.OnPush,
  template: `Precio: {{ producto.precio }}`
})
export class Precio { @Input({ required: true }) producto!: Producto; }

// padre
subirMutando() { this.producto.precio++; }                       // misma referencia → OnPush no se entera
subirInmutable() { this.producto = { ...this.producto, precio: this.producto.precio + 1 }; }  // nueva referencia

Nota: el clic del padre sí dispara la detección de cambios, pero el hijo OnPush la salta porque su input tiene la misma referencia.

Nivel 6 · Java 20 + Quarkus — tu proyecto "Farmacia API"

Preparación (una sola vez, 15 min)

  1. JDK 17 o superior. En tu máquina hay Java 8 y 11, y Quarkus 3 no corre con ellos. Instala Temurin 21: winget install EclipseAdoptium.Temurin.21.JDK. Escribe el código solo con lo que es estable en Java 20 (así coincide con lo que usan ellos).
  2. No necesitas instalar Maven: el proyecto trae mvnw.
  3. Genera el proyecto en code.quarkus.io con group com.farmacia, artifact farmacia-api y estas extensiones: REST Jackson, Hibernate ORM with Panache, JDBC Driver - H2, Hibernate Validator y SmallRye OpenAPI. Descarga el zip, descomprímelo y corre ./mvnw quarkus:dev.
  4. Abre http://localhost:8080/q/dev-ui. Si ves la Dev UI, estás listo.

J1 · Primer endpoint y entidad 15 min

Concepto: endpoint REST · Panache

Crea la entidad Producto (sku, nombre, precio, existencia) y un import.sql con 4 productos. Haz GET /productos, que devuelve todos.

Hecho cuando: http://localhost:8080/productos muestra el JSON con tus 4 productos.

Solución
// src/main/resources/application.properties
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:farmacia
quarkus.hibernate-orm.database.generation=drop-and-create

// src/main/resources/import.sql
INSERT INTO Producto(id, sku, nombre, precio, existencia) VALUES (1, 'A1', 'Paracetamol 500mg', 35, 12);
INSERT INTO Producto(id, sku, nombre, precio, existencia) VALUES (2, 'A2', 'Paracetamol infantil', 48, 0);
INSERT INTO Producto(id, sku, nombre, precio, existencia) VALUES (3, 'B1', 'Ibuprofeno 400mg', 52, 8);
INSERT INTO Producto(id, sku, nombre, precio, existencia) VALUES (4, 'C1', 'Omeprazol 20mg', 89, 5);
ALTER SEQUENCE Producto_SEQ RESTART WITH 50;

// Producto.java
@Entity
public class Producto extends PanacheEntity {
  public String sku;
  public String nombre;
  public BigDecimal precio;
  public int existencia;
}

// ProductoResource.java
@Path("/productos")
public class ProductoResource {
  @GET
  public List<Producto> listar() { return Producto.listAll(); }
}

J2 · DTO con record, servicio e inyección 15 min

Concepto: CDI · records · streams · Optional

Crea InventarioService (@ApplicationScoped) e inyéctalo en el resource. Agrega GET /productos?q=para (búsqueda por nombre) y GET /productos/{id} con 404 si no existe. Responde con un record ProductoDto que incluya agotado.

Hecho cuando: /productos?q=para devuelve 2 productos, el infantil con "agotado": true, y /productos/999 da 404.

Solución
public record ProductoDto(Long id, String sku, String nombre, BigDecimal precio, boolean agotado) {
  static ProductoDto from(Producto p) {
    return new ProductoDto(p.id, p.sku, p.nombre, p.precio, p.existencia == 0);
  }
}

@ApplicationScoped
public class InventarioService {
  public List<ProductoDto> buscar(String q) {
    return Producto.<Producto>list("lower(nombre) like ?1", "%" + q.toLowerCase() + "%")
        .stream().map(ProductoDto::from).toList();
  }
  public ProductoDto obtener(Long id) {
    return Producto.<Producto>findByIdOptional(id)
        .map(ProductoDto::from)
        .orElseThrow(NotFoundException::new);         // jakarta.ws.rs.NotFoundException → 404
  }
}

@Path("/productos")
public class ProductoResource {
  @Inject InventarioService inventario;

  @GET
  public List<ProductoDto> buscar(@QueryParam("q") @DefaultValue("") String q) { return inventario.buscar(q); }

  @GET @Path("/{id}")
  public ProductoDto obtener(@PathParam("id") Long id) { return inventario.obtener(id); }
}

J3 · Crear con validación 12 min

Concepto: POST · Bean Validation · @Transactional · 201 Created

POST /productos recibe un record ProductoInput validado y responde 201 con la ruta del nuevo producto. Pruébalo desde /q/swagger-ui con un precio negativo (debe dar 400) y con uno correcto (201).

Solución
public record ProductoInput(@NotBlank String sku, @NotBlank String nombre,
                            @Positive BigDecimal precio, @PositiveOrZero int existencia) {}

// InventarioService
@Transactional
public ProductoDto crear(ProductoInput in) {
  var p = new Producto();
  p.sku = in.sku(); p.nombre = in.nombre(); p.precio = in.precio(); p.existencia = in.existencia();
  p.persist();
  return ProductoDto.from(p);
}

// ProductoResource
@POST
public Response crear(@Valid ProductoInput input) {
  var p = inventario.crear(input);
  return Response.created(URI.create("/productos/" + p.id())).entity(p).build();
}

J4 · Regla de negocio y error propio 12 min

Concepto: excepciones · @ServerExceptionMapper · 409

POST /productos/{id}/descontar?cantidad=3 descuenta existencia. Si no alcanza, lanza StockInsuficienteException y responde 409 con {"error": "..."}.

Hecho cuando: descontar 3 de un producto con 12 da 200 y el nuevo valor; descontar 50 da 409 con el mensaje.

Solución
public class StockInsuficienteException extends RuntimeException {
  public StockInsuficienteException(String sku, int hay, int pides) {
    super("Stock insuficiente de %s: hay %d, pides %d".formatted(sku, hay, pides));
  }
}

// InventarioService
@Transactional
public ProductoDto descontar(Long id, int cantidad) {
  Producto p = Producto.<Producto>findByIdOptional(id).orElseThrow(NotFoundException::new);
  if (cantidad > p.existencia) throw new StockInsuficienteException(p.sku, p.existencia, cantidad);
  p.existencia -= cantidad;                        // entidad administrada: se guarda al cerrar la transacción
  return ProductoDto.from(p);
}

// ProductoResource
@POST @Path("/{id}/descontar")
public ProductoDto descontar(@PathParam("id") Long id, @QueryParam("cantidad") int cantidad) {
  return inventario.descontar(id, cantidad);
}

// ErroresMapper.java
public class ErroresMapper {
  @ServerExceptionMapper
  public RestResponse<Map<String, String>> stock(StockInsuficienteException e) {
    return RestResponse.status(Response.Status.CONFLICT, Map.of("error", e.getMessage()));
  }
}

J5 · Pruebas con @QuarkusTest 12 min

Concepto: JUnit 5 · RestAssured · camino feliz y de error

Tres pruebas: la búsqueda responde 200 con resultados, un id inexistente responde 404, y crear con precio negativo responde 400. Córrelas con ./mvnw test.

Solución
@QuarkusTest
class ProductoResourceTest {
  @Test
  void buscaPorNombre() {
    given().queryParam("q", "para")
      .when().get("/productos")
      .then().statusCode(200).body("size()", is(2));
  }

  @Test
  void idInexistenteDevuelve404() {
    given().when().get("/productos/9999").then().statusCode(404);
  }

  @Test
  void precioNegativoDevuelve400() {
    given().contentType(ContentType.JSON)
      .body("{\"sku\":\"X1\",\"nombre\":\"Prueba\",\"precio\":-5,\"existencia\":1}")
      .when().post("/productos")
      .then().statusCode(400);
  }
}
// imports: static io.restassured.RestAssured.given · static org.hamcrest.Matchers.is · io.restassured.http.ContentType

J6 · Conectarlo con Angular 15 min

Concepto: CORS · contrato OpenAPI · integración front-back

Habilita CORS para localhost:4200 en el perfil dev. En tu proyecto practica, cambia el FakeApi del buscador (D6) por un servicio real que llame a http://localhost:8080/productos?q=. Copia la interfaz del DTO desde /q/swagger-ui.

Hecho cuando: el buscador de Angular muestra productos que vienen de tu API Quarkus. Toma una captura: es tu evidencia del proyecto.

Solución
# application.properties (Quarkus)
%dev.quarkus.http.cors.enabled=true
%dev.quarkus.http.cors.origins=http://localhost:4200
# en versiones anteriores de Quarkus 3: quarkus.http.cors=true

// Angular: productos-api.ts
export interface ProductoDto { id: number; sku: string; nombre: string; precio: number; agotado: boolean; }

@Injectable({ providedIn: 'root' })
export class ProductosApi {
  private http = inject(HttpClient);
  buscar(q: string) {
    return this.http.get<ProductoDto[]>('http://localhost:8080/productos', { params: { q } });
  }
}
// app.config.ts: provideHttpClient()
// En el buscador: inject(ProductosApi) en lugar de FakeApi; el resto del pipe no cambia.

Sobre ti

Cómo responder cualquier pregunta técnica

Concepto en una frase, pegado a dónde lo usaste y qué resolvía. En 20 a 30 segundos, y al final ofrece: "¿Quieres que entre al detalle?". Si algo no lo has usado: "El concepto es [X]. No lo he usado en producción, pero lo aplicaría en [caso]."

"Cuéntame de ti" (60–90 s)

"Soy desarrollador front con más de cinco años de experiencia, especializado en Angular. Los últimos cuatro años y medio trabajé en una consultora, asignado a una cadena farmacéutica con 86 sucursales: primero en el punto de venta, que usan más de 500 terminales, y después en el backoffice. En el punto de venta hice las actualizaciones de Angular de la 17 a la 19, y en el backoffice lideré la migración de los módulos a Angular 20 con microfrontends y Native Federation, con una librería compartida entre módulos; fui el mayor contribuidor de ese repositorio. En mi día a día uso Signals, RxJS, Reactive Forms, interceptores y lazy loading, que es lo que pide esta vacante. También hago backend en .NET, así que entiendo bien el contrato con las APIs. Busco un proyecto enfocado en construir componentes reutilizables y una arquitectura de front que escale."

Historias STAR

Arquitectura modular → la migración a microfrontends

Situación: un backoffice de 16 áreas en una sola aplicación; cualquier cambio obligaba a construir y desplegar todo. Tarea: liderar la migración. Acción: separé cada área en un remoto con Native Federation, un shell que los carga desde un manifiesto, y moví lo común — buscador de productos, loading, manejo de errores — a una librería compartida. Resultado: cada módulo se construye y despliega solo. Hice alrededor de dos tercios de los commits del repositorio; los módulos que más trabajé fueron pedido manual — el primero en integrarse —, login, home, recepción electrónica y monitor de pedidos.

Si preguntan por tiempos de build: "No lo medí formalmente; el cambio fue que modificar pedidos ya no obligaba a recompilar los otros quince módulos."

Manejo de errores → el ErrorHandler global

Situación: más de 500 terminales; cuando algo fallaba en la caja, soporte solo recibía "no me deja cobrar". Tarea: que cada error llegara con lo necesario para reproducirlo. Acción: creé un ErrorHandler global que envía cada error no controlado a un servicio de log con sucursal, caja, IP, versión, URL, y el método y la línea del stack. Los errores de carga de módulos lazy después de un despliegue se registran aparte. Resultado: se diagnostica desde el log sin llamar a la sucursal, y se distingue un bug de un problema de despliegue.

Componente reutilizable → la directiva contra el doble clic

Situación: un doble clic en "Cobrar" o "Cancelar pedido" enviaba la operación dos veces. Tarea: resolverlo en todos los botones sin reescribirlos. Acción: una directiva que intercepta el clic, lo pasa por un Subject con throttleTime configurable y emite un evento propio; la puse en los botones compartidos de pago. Resultado: hoy está en 23 botones de pago, envío y cancelación. Hoy la haría con exhaustMap, para bloquear mientras dure la petición y no por un tiempo fijo.

Mantener Angular al día → las actualizaciones del punto de venta

Situación: el punto de venta estaba en Angular 17. Tarea: actualizarlo sin afectar la venta en las sucursales. Acción: una versión mayor a la vez con ng update: de 17 a 18 y luego de 18 a 19.2, que tocó 204 archivos; lo que los schematics no migraron lo corregí a mano. Resultado: el punto de venta quedó en 19.2 sin interrumpir la operación.

Un error propio → el spinner que bloqueaba la caja

Situación: al buscar un producto o agregarlo al carrito, si el servicio fallaba el spinner se quedaba en pantalla y el cajero tenía que recargar. Tarea: encontrar la causa. Acción: el spinner se ocultaba solo cuando la petición salía bien — era un patrón que yo también usaba —; agregué el manejo del caso de error en la búsqueda y en el carrito. Resultado y aprendizaje: desde entonces cierro cualquier estado de carga en finalize, que corre en éxito, error y cancelación, y en revisiones de código pregunto: ¿qué pasa en pantalla si esto falla?

Preguntas de la reclutadora

PreguntaQué respondes
Pretensiones salariales"Busco entre 40 y 45 mil brutos mensuales, según el esquema de prestaciones. ¿Qué rango tienen para la posición?" (Semi Sr Angular en consultora en CDMX suele andar entre 35 y 48 mil; es una estimación de mercado.)
Modalidad híbrida en CDMXDecide antes cuál es la tuya y sostenla:
"Tengo disponibilidad para reubicarme a CDMX en unas dos semanas."
"Puedo ir a oficina cuando el proyecto lo requiera, con aviso; mi base es remota."
"Mi modalidad es remota. ¿Tienen alguna posición remota con este cliente?"
Escolaridad"Soy pasante de Ingeniería en Tecnologías de la Información y Comunicaciones por el Tecnológico de Tlalnepantla; la titulación está en trámite. ¿El cliente acepta carta de pasante?"
Inglés técnico"Leo documentación y escribo tickets, commits y correos en inglés sin problema."
Horario 8:30–18:00"Sin problema."
Disponibilidad"Inmediata."
¿Por qué saliste?"Terminó la asignación con el cliente. Cerré la migración que lideraba y busco un proyecto de front donde aportar lo mismo."
Prestaciones (PL y PLS)Pregunta: "¿Cuánto es el fondo de ahorro, el monto de vales y la suma asegurada del seguro de gastos médicos? ¿La contratación es directa con Capgemini?"

Tus preguntas al final (elige 3)

Repaso activo

Lee la pregunta, responde en voz alta antes de abrir la respuesta y márcala. Las que marques "Repasar" son las que haces al día siguiente. Tu avance se guarda en este navegador.

Signals · ¿Diferencia entre signal, computed y effect?
Ver respuesta

signal = estado escribible; computed = derivado, memoizado y de solo lectura; effect = efecto hacia fuera de Angular (storage, log).

Signals · ¿Cómo agregas un elemento a un signal de arreglo?
Ver respuesta

items.update(xs => [...xs, nuevo]) — arreglo nuevo, nunca push.

Signals · ¿Qué es linkedSignal?
Ver respuesta

Un signal escribible que se reinicia cuando cambia su fuente (página que vuelve a 1 al cambiar el filtro).

Signals · ¿Cómo conviertes un Observable en signal y al revés?
Ver respuesta

toSignal(obs$, { initialValue }) y toObservable(signal).

Signals · ¿input(), output() y model()?
Ver respuesta

Input como signal (input.required obligatorio); evento hacia el padre; binding de doble vía.

Signals · ¿Cómo se activa zoneless?
Ver respuesta

provideZonelessChangeDetection(), estable desde Angular 20.2.

RxJS · ¿Qué operador para un buscador?
Ver respuesta

switchMap: cancela la búsqueda anterior. Antes: debounceTime y distinctUntilChanged.

RxJS · ¿Qué operador para un botón de cobrar?
Ver respuesta

exhaustMap: ignora clics mientras la petición sigue en curso.

RxJS · ¿Qué operador para guardados que deben ir en orden?
Ver respuesta

concatMap: los pone en cola.

RxJS · ¿forkJoin o combineLatest?
Ver respuesta

forkJoin emite una vez cuando todos terminan; combineLatest emite cada vez que cualquiera cambia.

RxJS · ¿Tres formas de evitar fugas de memoria?
Ver respuesta

async pipe / toSignal; takeUntilDestroyed() al final del pipe; takeUntil(destroy$) en código anterior.

RxJS · ¿Dónde va el catchError en un buscador y por qué?
Ver respuesta

Dentro del switchMap: un error completa el observable; adentro solo muere esa petición.

RxJS · ¿Subject vs BehaviorSubject?
Ver respuesta

Subject no guarda valor (eventos); BehaviorSubject guarda el último y necesita valor inicial (estado).

Forms · ¿Qué devuelve un validador cuando es válido?
Ver respuesta

null. Si no, un objeto con el error: { rfc: true }.

Forms · ¿Dónde va un validador que compara dos campos?
Ver respuesta

En el FormGroup: fb.group({...}, { validators: rangoFechas('desde','hasta') }).

Forms · ¿value o getRawValue?
Ver respuesta

value excluye deshabilitados; getRawValue trae todo.

Forms · ¿setValue o patchValue?
Ver respuesta

setValue exige todos los campos; patchValue acepta parcial.

Forms · ¿Cómo enlazas un FormArray en la plantilla?
Ver respuesta

formArrayName="lineas" y dentro [formGroupName]="i" por cada control.

Forms · ¿Cómo evitas que un validador asíncrono consulte en cada tecla?
Ver respuesta

updateOn: 'blur' en el control.

Reutilizables · ¿Los 4 métodos de ControlValueAccessor?
Ver respuesta

writeValue, registerOnChange, registerOnTouched, setDisabledState + provider NG_VALUE_ACCESSOR con multi: true.

Reutilizables · ¿Cómo proyectas contenido en varias zonas?
Ver respuesta

<ng-content select="[card-title]"> por zona y un ng-content sin select para el resto.

Reutilizables · ¿Cómo logras consistencia visual?
Ver respuesta

Tokens en variables CSS usados por todos los componentes; tema de Material 3 por tokens; catálogo tipo Storybook.

Reutilizables · ¿Directiva o componente?
Ver respuesta

Directiva agrega comportamiento a un elemento existente; componente tiene plantilla propia.

HTTP · ¿Cómo registras interceptores funcionales?
Ver respuesta

provideHttpClient(withInterceptors([auth, loading, error])) en app.config.ts.

HTTP · ¿Cómo agregas una cabecera en un interceptor?
Ver respuesta

next(req.clone({ setHeaders: { Authorization: 'Bearer ' + t } })) — la petición es inmutable.

HTTP · ¿Por qué un contador para el loading global?
Ver respuesta

Con booleano, la primera petición que termina apaga el spinner de las demás. Se baja en finalize.

HTTP · ¿Interceptor o ErrorHandler?
Ver respuesta

Interceptor: errores HTTP, reacciona por petición. ErrorHandler: cualquier error no controlado, para registrarlo.

Rendimiento · ¿Cuándo se actualiza un componente OnPush?
Ver respuesta

Nueva referencia en un input, evento en su plantilla, async pipe, signal que lee, o markForCheck.

Rendimiento · ¿Para qué sirve track en @for?
Ver respuesta

Identificar cada elemento para no recrear el DOM al llegar una lista nueva. Obligatorio en @for.

Rendimiento · ¿Lazy loading vs @defer?
Ver respuesta

Lazy separa por ruta (loadChildren/loadComponent); @defer separa partes de una pantalla (on viewport, interaction…).

Rendimiento · ¿Por qué evitar métodos en la plantilla?
Ver respuesta

Se ejecutan en cada ciclo; usa pipe puro o computed.

Arquitectura · ¿Qué va en Core, Shared y Feature?
Ver respuesta

Core: una sola vez (interceptores, guards, sesión, layout). Shared: UI reutilizable sin negocio. Feature: área de negocio lazy.

Arquitectura · ¿Regla de dependencias?
Ver respuesta

Feature usa Shared y Core; nunca importa de otro feature. Lo compartido se sube a Shared.

Arquitectura · ¿Cómo provees estado solo para un feature?
Ver respuesta

En providers de la ruta del feature: vive mientras estás en él.

Java · ¿Java 20 es LTS?
Ver respuesta

No. La LTS es 21. En 20, virtual threads y pattern matching en switch eran preview.

Quarkus · ¿Con qué anotaciones haces un endpoint GET con parámetro de ruta?
Ver respuesta

@Path("/productos") en la clase, @GET @Path("/{id}") en el método y @PathParam("id") en el parámetro.

Quarkus · ¿Cómo inyectas un servicio?
Ver respuesta

El servicio con @ApplicationScoped; en el resource, @Inject InventarioService inventario; (CDI, resuelto al compilar).

Quarkus · ¿Qué te da PanacheEntity?
Ver respuesta

id, persist(), findById(), findByIdOptional(), listAll(), list() y find(). Escrituras en @Transactional.

Quarkus · ¿Cómo validas la entrada y qué responde si falla?
Ver respuesta

Anotaciones de Bean Validation (@NotBlank, @Positive) en el record y @Valid en el parámetro. Responde 400 automáticamente.

Quarkus · ¿Cómo conviertes tu excepción en un 409?
Ver respuesta

Un método con @ServerExceptionMapper que devuelve RestResponse.status(CONFLICT, ...).

Quarkus · ¿Dónde ves el contrato y cómo pruebas un endpoint?
Ver respuesta

/q/openapi y /q/swagger-ui. Pruebas con @QuarkusTest + RestAssured: given().when().get(...).then().statusCode(200).

Java · ¿Cómo transformas una lista de entidades a DTOs?
Ver respuesta

lista.stream().map(ProductoDto::from).toList() — como un Select de LINQ.

Testing · ¿Cómo pruebas un servicio HTTP?
Ver respuesta

provideHttpClient() + provideHttpClientTesting(), HttpTestingController.expectOne, flush, verify.

CLI · ¿Cómo generas un componente, un servicio y un interceptor?
Ver respuesta

ng g c ruta/nombre · ng g s ruta/nombre · ng g interceptor ruta/nombre.

CLI · ¿Cómo compilas para producción y actualizas de versión?
Ver respuesta

ng build --configuration production · ng update @angular/core @angular/cli, una versión mayor a la vez.

npm · ¿npm install o npm ci?
Ver respuesta

ci instala exacto desde el lockfile y es reproducible: para pipelines.

Git · ¿Merge o rebase?
Ver respuesta

Merge une historias y es seguro en ramas compartidas; rebase reescribe y deja historia lineal: solo en tu rama local.

Git · ¿revert o reset?
Ver respuesta

revert para lo que ya está en el remoto (crea un commit nuevo); reset solo para commits locales.

Git · ¿Cómo guardas cambios a medias para cambiar de rama?
Ver respuesta

git stash y después git stash pop.

Git · ¿Cómo llevas un solo commit a otra rama?
Ver respuesta

git cherry-pick <hash>.

Simulacro — 24 preguntas en voz alta, grabándote

  1. Cuéntame de ti.
  2. ¿Cómo estructuras un proyecto Angular?
  3. ¿Qué cambió con los componentes standalone?
  4. ¿Qué es un signal? ¿signal, computed y effect?
  5. ¿Signals reemplaza a RxJS?
  6. ¿switchMap, mergeMap, concatMap y exhaustMap, con un caso de cada uno?
  7. ¿Cómo evitas fugas de memoria?
  8. ¿Por qué Reactive Forms?
  9. ¿Cómo haces un validador propio? ¿Y uno que compare dos campos?
  10. ¿Qué es ControlValueAccessor?
  11. ¿Qué hace reutilizable a un componente?
  12. ¿Cómo mantienes la consistencia visual?
  13. ¿Qué hacen tus interceptores?
  14. ¿Cómo funciona OnPush?
  15. ¿Para qué sirve track?
  16. Una pantalla va lenta, ¿qué haces?
  17. ¿Haces pruebas unitarias?
  18. ¿Qué comandos del CLI usas en el día a día?
  19. ¿Cómo es tu flujo de trabajo con Git? ¿Merge o rebase?
  20. ¿Has trabajado con Java o Quarkus?
  21. ¿Cómo harías un endpoint en Quarkus que valide lo que recibe?
  22. ¿Cómo conectas Angular con un backend Quarkus?
  23. Cuéntame de un error tuyo.
  24. ¿Qué pretensiones salariales tienes?

Checklist del día