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 luego tu caso.
  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

Así se evalúa en una entrevista: primero que entiendes el concepto y luego que lo has usado de verdad. Cada respuesta son dos partes que dices seguidas, en 20–30 segundos. Si quieren más, preguntan; para eso está "Si repreguntan":

Concepto Una frase: qué es.
Cómo lo apliqué Dos o tres frases: dónde lo usaste y para qué.

Todos los casos salen del historial de git de tus dos proyectos. Donde dice "hice / creé", el archivo lo creaste tú; donde dice "en el módulo X usamos" o "trabajé en", es código del equipo en el que participaste. Cuando no hay caso real, la etiqueta es "Dónde lo aplicaría": dilo así, sin inventar.

1 · Arquitectura modular — Core, Shared, Feature

¿Cómo estructuras un proyecto Angular?

Concepto En tres carpetas. core: lo que existe una sola vez en la app — interceptores, guards, sesión. shared: lo reutilizable sin lógica de negocio — componentes, pipes, validadores. features: una carpeta por área de negocio, cargada con lazy loading. La regla: un feature usa core y shared, pero nunca importa de otro feature.
Cómo lo apliqué Así está el punto de venta: catorce features lazy como pago o entrega. En el backoffice cada feature es un microfrontend con Native Federation y lo común vive en una librería compartida.
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?

Concepto Un valor reactivo: cuando cambia, Angular actualiza solo lo que depende de él. signal guarda estado y computed lo deriva.
Cómo lo apliqué El buscador que creé para la librería del backoffice guarda sus resultados y su estado en signals. En inventarios, si se puede agregar un lote es un computed: sin suscripciones que limpiar.
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?

Concepto El padre pasa datos con input() y el hijo avisa con output().
Cómo lo apliqué Mi buscador recibe su etiqueta por input() y emite el producto elegido por output(). No sabe qué módulo lo usa.

3 · RxJS

¿Observable o Promesa?

Concepto La promesa da un valor y no se cancela; el Observable da varios en el tiempo y sí se cancela.
Cómo lo apliqué En el backoffice usamos promesas para operaciones de un disparo, como guardar un pedido, y RxJS para flujos continuos, como lo que se teclea en el buscador.

¿switchMap, mergeMap, concatMap o exhaustMap?

Concepto Cambia qué hacen si llega un valor nuevo: switchMap cancela el anterior, concatMap hace cola, mergeMap va en paralelo y exhaustMap ignora hasta terminar.
Cómo lo apliqué Para el doble clic en "Cobrar" hice una directiva con throttleTime, hoy en 23 botones; ahora usaría exhaustMap. En mi buscador cambiaría la promesa por switchMap para cancelar búsquedas viejas.
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?

Concepto Con forkJoin: las lanza en paralelo y emite una vez, cuando todas terminan.
Cómo lo apliqué En el punto de venta, al agregar un producto se lanzan siete consultas a la vez — existencia, precio, alertas, lealtad, entre otras — y se valida que ninguna falle. Tarda lo que la más lenta, no la suma.

¿Cómo evitas fugas de memoria?

Concepto Cerrando cada suscripción: con async pipe, toSignal o takeUntilDestroyed().
Cómo lo apliqué En mi buscador uso takeUntil(destroy$) y lo emito en ngOnDestroy. Hoy lo cambiaría por takeUntilDestroyed(), que es lo mismo sin el Subject.
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?

Concepto Porque el formulario vive en TypeScript: tipado, fácil de validar y de probar.
Cómo lo apliqué En los dos proyectos todo es reactive: pago, apertura de caja, captura de pedidos. Son formularios largos con reglas que dependen de otros campos.

¿Cómo haces un validador personalizado?

Concepto Una función que devuelve null si es válido o un objeto con el error.
Cómo lo apliqué En la apertura de caja tenemos shouldBeLess: el fondo inicial no puede pasar del máximo de la sucursal. Se aplica al grupo y la plantilla muestra el aviso.

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

Concepto Con setValidators o clearValidators y luego updateValueAndValidity().
Cómo lo apliqué En el pago con tarjeta, si es Amex los 4 dígitos se vuelven obligatorios; con otra tarjeta se quitan las validaciones y el campo se deshabilita.

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

Concepto Con FormArray: agregas con push y quitas con removeAt.
Cómo lo apliqué El modal de apego del punto de venta tiene un FormArray de renglones, y el alta de contactos también.
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?

Concepto API chica con input y output, sin lógica de negocio adentro, y estilos de tokens compartidos.
Cómo lo apliqué Creé el buscador de productos de la librería: se configura con inputs, emite el producto y esconde lo difícil — debounce y paginación infinita. Varios módulos lo usan sin saber cómo funciona por dentro.

¿Qué es ControlValueAccessor?

Concepto La interfaz que hace que tu componente funcione con formControlName como un input normal.
Dónde lo aplicaría Mi buscador hoy avisa con un output. Con ControlValueAccessor se usaría directo en el formulario, con validaciones incluidas. Es la mejora que le haría.

¿Cómo mantienes la consistencia visual?

Concepto Con tokens de diseño — variables de color y espaciado — y componentes base compartidos.
Cómo lo apliqué En el punto de venta hay botones compartidos de pago que usan todas las pantallas; ahí puse la directiva contra el doble clic y llegó a toda la app de una vez.

6 · APIs REST e interceptores

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

Concepto Una función que pasa por cada petición HTTP: token, errores, loading, logs.
Cómo lo apliqué El punto de venta tiene cuatro: token, timeout, logging y contexto de venta; en el backoffice el de autenticación ya es funcional. Además creé un ErrorHandler global que registra cada error con sucursal, caja y línea.
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?

Concepto El componente solo se revisa si cambia la referencia de un input, hay un evento o cambia un signal.
Cómo lo apliqué En el backoffice unos 32 componentes son OnPush; por eso trabajamos con datos inmutables.

¿Para qué sirve trackBy o track?

Concepto Identifica cada elemento de la lista para no recrear el DOM completo.
Cómo lo apliqué En los monitores de pedidos y de embarques, las tablas se refrescan del servidor con trackBy por folio, sin reconstruirse enteras.

¿Lazy loading o @defer?

Concepto Lazy loading carga por ruta; @defer carga partes de una misma pantalla.
Cómo lo apliqué Lazy loading en los dos proyectos: en el backoffice cada microfrontend se descarga solo cuando el usuario entra.
Dónde aplicaría @defer En pantallas con gráficas o tablas pesadas debajo del primer scroll, como un resumen de facturas: @defer (on viewport) con un placeholder.

Una pantalla va lenta, ¿qué haces?

Concepto Primero mido con Angular DevTools y la pestaña Network; luego corrijo y vuelvo a medir.
Cómo lo apliqué En el buscador puse debounce y mínimo de caracteres para no consultar en cada tecla, y paginación para no traer todo de golpe.

8 · Testing

¿Haces pruebas unitarias?

Concepto Sí: pruebo cada unidad aislada, simulando sus dependencias.
Cómo lo apliqué Con Jasmine y Karma, y SonarQube en el pipeline. Por ejemplo, probé el servicio que guarda un pedido en sus tres caminos: éxito, error de negocio y excepción.

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 la entrevista llega antes de que termines el 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."

Con esa respuesta, los bloques "Concepto" de esta sección te siguen sirviendo si repreguntan. Los "Cómo lo apliqué · proyecto propio" úsalos solo cuando hayas hecho J1–J6.

¿Has trabajado con Java o Quarkus?

Concepto Quarkus es un framework Java para APIs REST, rápido y pensado para contenedores.
Cómo lo apliqué · proyecto propio En producción no; mi backend fue .NET. Pero hice una API de práctica con Quarkus: inventario de farmacia con validaciones, manejo de errores y pruebas, lista para conectarse con Angular.

¿Cómo haces un endpoint REST en Quarkus?

Concepto Con anotaciones: @Path en la clase y @GET o @POST en los métodos.
Cómo lo apliqué · proyecto propio En mi API hice /productos con búsqueda, obtener por id con 404 si no existe, y crear con 201. Es igual a un controller de .NET.
@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?

Concepto Con CDI: la clase lleva @ApplicationScoped y se pide con @Inject.
Cómo lo apliqué · proyecto propio El resource solo recibe y responde; la lógica está en un InventarioService inyectado, igual que separo en Angular.

¿Cómo accedes a la base de datos?

Concepto Con Hibernate Panache: la entidad ya trae persist, findById y list.
Cómo lo apliqué · proyecto propio Mi entidad Producto usa Panache con H2, y descontar existencia es @Transactional: si falla la regla, no se guarda nada.
@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?

Concepto Con anotaciones como @NotBlank o @Positive y @Valid; si falla, responde 400 solo.
Cómo lo apliqué · proyecto propio Si mando un precio negativo a mi API, regresa 400 con el campo que falló. El front valida para el usuario; el backend, por seguridad.
public record ProductoInput(
  @NotBlank String sku,
  @NotBlank String nombre,
  @Positive BigDecimal precio,
  @PositiveOrZero int existencia) {}

¿Cómo manejas los errores en la API?

Concepto Excepciones propias que un mapeador convierte en el código HTTP correcto.
Cómo lo apliqué · proyecto propio Si piden más existencia de la que hay, lanzo StockInsuficienteException y responde 409 con un mensaje que Angular muestra.
@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?

Concepto Records, streams con lambdas y Optional.
Cómo lo apliqué · proyecto propio Mis DTO son record y convierto listas con stream().map(...).toList(). Viniendo de C#, es como LINQ.
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?

Concepto Con @QuarkusTest y RestAssured: haces la petición y revisas el status.
Cómo lo apliqué · proyecto propio Probé los tres caminos de mi endpoint: 200 con resultados, 404 si no existe y 400 si el dato es inválido.
@QuarkusTest
class ProductoResourceTest {
  @Test
  void idInexistenteDevuelve404() {
    given().when().get("/productos/9999").then().statusCode(404);
  }
}

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

Concepto Por contrato OpenAPI y con CORS habilitado para el front.
Cómo lo apliqué · proyecto propio En mi API habilité CORS solo para localhost:4200 y lo verifiqué con una prueba; los tipos TypeScript salen del contrato en Swagger.
# 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?

Concepto 21 es LTS, con soporte largo; 20 no, y lo que en 20 era preview en 21 es estable.
Cómo lo apliqué · proyecto propio Como me dijeron que usan Java 20, en mi API usé solo lo estable en la 20: 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?

Concepto ng serve, ng generate, ng build, ng test y ng update.
Cómo lo apliqué Compilo por ambiente — desarrollo, QA y producción — y las actualizaciones de Angular 17 a 19 las hice con ng update, una versión a la vez.
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?

Concepto Una rama por tarea, commits chicos y todo entra por Pull Request con revisión.
Cómo lo apliqué En Azure DevOps, cada work item tenía su rama desde DEV; antes de cerrar el PR traía DEV a mi rama y resolvía conflictos ahí.
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 (una frase) → Cómo lo apliqué (dos o tres frases). En 20 a 30 segundos, y al final ofrece: "¿Quieres que entre al detalle?". Si algo no lo has usado: "En producción no lo he usado; el concepto es [X] y donde lo aplicaría es [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 OnPush, 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