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 menos de 15 segundos: concepto y un ejemplo.
  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 son dos frases cortas, menos de 15 segundos: el concepto y un ejemplo donde lo usaste. Mejor algo corto y dicho con seguridad que algo largo en lo que te trabas. 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 En tres carpetas: core, lo que existe una vez, como guards e interceptores; shared, lo reutilizable, como modales; y features, una por área, con lazy loading. Así está el punto de venta: tienda, pago y entrega son features.
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? — La idea de las tres capas no cambia; cambia que cada componente se importa directo donde se usa, sin NgModules. En el backoffice todo es standalone: compra directa importa el buscador de la librería directamente en su imports. El punto de venta sigue con NgModules, que siguen soportados.

¿Cómo evitas que un feature importe de otro? — Poniendo lo compartido en una capa común y prohibiendo imports de lado. En el backoffice cada área es un proyecto aparte, así que físicamente no puede importar de otra: lo que necesitan dos áreas, como el buscador, se sube a la librería común y se importa de libreria.

¿Dónde pones el estado? — Lo más cerca de quien lo usa. Local: en recepción electrónica, los lotes que se están capturando viven en signals del propio componente. Global: en el punto de venta, el cliente de la venta vive en un servicio de root, porque lo leen las pantallas de contacto y de lealtad.

¿Por qué microfrontends? — Para que cada área se construya y se despliegue sola. En el backoffice eran 16 áreas con ritmos distintos: un cambio en devoluciones ya no obligaba a volver a desplegar compra directa. 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 valor reactivo: cuando cambia, Angular actualiza las partes que dependen de él. En recepción electrónica, un computed habilita "Agregar lote" solo si hay lote, piezas y caducidad.
Si repreguntan

¿Cuándo usas effect? — Principalmente para sincronizar estado reactivo con algo externo. En recepción electrónica, un effect guarda en sessionStorage el proveedor, la fecha y las líneas capturadas cuando cambian, para no perder la captura si el usuario recarga.

¿Cómo actualizas un arreglo? — Con update, devolviendo un arreglo nuevo. Si hago push, la mutación no pasa por set ni update, y aunque lo hiciera dentro de update, devolver el mismo arreglo no notifica porque compara por referencia. En compra directa: lineasProducto.update(ls => [...ls, nueva]).

¿Signals reemplaza a RxJS? — No, se complementan: signals para estado, RxJS para eventos en el tiempo. Mi buscador usa los dos: lo que se teclea pasa por un Subject con debounceTime, que es RxJS, y los resultados y el "buscando…" son signals que lee la plantilla.

¿linkedSignal? — Un signal escribible cuyo valor se deriva de una fuente y se reinicia cuando esa fuente cambia. No lo he usado en producción; lo aplicaría en consulta de movimientos de inventario: hoy un effect limpia filtros y fechas al cambiar de producto, y con linkedSignal ese reinicio sería declarativo.

¿Zoneless? — Ejecutar Angular sin Zone.js: la detección de cambios la disparan las notificaciones de Angular, como signals y eventos (provideZonelessChangeDetection(), estable desde 20.2). En el backoffice seguimos con Zone y eventCoalescing; como el código nuevo ya usa signals, sería el siguiente paso.

¿Cómo comunicas componentes padre e hijo?

Respuesta El padre manda datos con input y el hijo avisa con output. En devoluciones, la pantalla le pasa la etiqueta a mi buscador, y el buscador le devuelve el producto elegido para agregarlo como renglón.
Devolución (padre) ── input:  label = "Agregar producto:" ──▶ Buscador (hijo)
Devolución (padre) ◀── output: producto elegido ───────────── Buscador (hijo)
                        └─ el padre agrega un renglón a la devolución

<!-- plantilla real de la devolución -->
<app-product-search label="Agregar producto:"
                    (onProductSelected)="onProductoSeleccionado($event)"></app-product-search>

3 · RxJS

¿Observable o Promesa?

Respuesta Una Promise resuelve una vez; un Observable puede emitir varios valores, es lazy y puedes cancelar la suscripción. Guardar un pedido es una promesa; lo que se teclea en el buscador es un Observable.

¿switchMap, mergeMap, concatMap o exhaustMap?

Respuesta switchMap se desuscribe del anterior, concatMap hace cola, mergeMap va en paralelo y exhaustMap ignora lo nuevo hasta terminar. El doble clic en "Cobrar" lo resolví con una directiva; hoy exhaustMap sería la opción.
Si repreguntan

¿switchMap realmente cancela la petición HTTP? — switchMap se desuscribe del Observable anterior, y con HttpClient esa desuscripción aborta la petición en curso. En un buscador, la respuesta vieja nunca llega a pisar a la nueva.

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: emite una vez, cuando todas completan, y si una falla sin manejar el error, falla todo. Al agregar un producto en subrogación, consulta a la vez existencia, precio y promociones.

¿Cómo evitas fugas de memoria?

Respuesta Si me suscribo a mano, cierro la suscripción con el ciclo de vida, por ejemplo con takeUntilDestroyed. Mi buscador deja de escuchar el teclado al destruirse la pantalla.
Si repreguntan

¿Dónde va el catchError? — Depende de qué quiero recuperar: dentro de cada petición si acepto resultados parciales; afuera si un fallo invalida todo. En subrogación va afuera del forkJoin, porque si falla una de las consultas el producto no se puede agregar: marca la respuesta como fallida y el cajero ve el mensaje.

¿Subject o BehaviorSubject? — Subject no guarda nada, sirve para eventos; BehaviorSubject guarda el último valor, sirve para estado. En el punto de venta, el cliente de la venta está en un BehaviorSubject: cuando el cajero abre la pantalla de lealtad después de elegir al cliente, recibe el cliente actual al suscribirse. Con un Subject no recibiría nada hasta el siguiente cambio.

¿Cold vs hot? — Un Observable cold ejecuta su productor por cada suscripción; uno hot comparte una fuente que existe aparte. En mi buscador, el Subject del teclado es hot y cada petición de HttpClient es cold: se ejecuta al suscribirse.

4 · Reactive Forms

¿Por qué Reactive Forms?

Respuesta Porque el formulario vive en TypeScript y cambio sus reglas desde el código. En el cobro, las validaciones cambian según la tarjeta que elige el cajero.

¿Cómo haces un validador personalizado?

Respuesta Una ValidatorFn que devuelve null si es válido, o un objeto con el error. Al abrir caja, uno impide que el fondo pase del máximo de la sucursal.

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

Respuesta Con setValidators o clearValidators. Si el cajero marca Amex, el campo de 4 dígitos se vuelve obligatorio; si no, se deshabilita.

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

Respuesta Con FormArray, agregando o quitando renglones. En el alta de cliente, cada teléfono es un renglón.
Si repreguntan

¿value o getRawValue? — value excluye los campos deshabilitados; getRawValue() trae todo. En pedido manual, el proveedor se deshabilita en cuanto hay líneas, para que no lo cambien a medias; por eso al guardar se lee con getRawValue(), o se perdería.

¿setValue o patchValue? — setValue exige todos los campos; patchValue acepta solo algunos. En el alta de cliente del punto de venta, si la fecha de nacimiento no corresponde a un adulto, patchValue limpia solo ese campo y deja intacto lo demás que el cajero ya capturó.

¿Errores al enviar? — Con markAllAsTouched(), que marca todos los campos y hace aparecer sus mensajes. En el login del backoffice, si el usuario da "Entrar" con campos vacíos, se marcan todos y ve de una vez qué le falta.

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. Mi buscador se usa en compra directa y en devoluciones: cada pantalla solo recibe el producto elegido.

¿Qué es ControlValueAccessor?

Respuesta · sin caso en producción Hace que tu componente funcione con formControlName. No lo he usado; se lo pondría a mi buscador para usarlo como un campo más del formulario.
Si repreguntan

¿Qué métodos tiene? — writeValue (el formulario le da un valor), registerOnChange (para avisar cambios), registerOnTouched (para marcarlo tocado) y, cuando aplica, setDisabledState. Se registra con el provider NG_VALUE_ACCESSOR.

¿Cómo mantienes la consistencia visual?

Respuesta Con componentes base compartidos y estilos comunes. En el punto de venta, todas las pantallas usan los mismos componentes de botones de pago.

6 · APIs REST e interceptores

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

Respuesta Intercepta peticiones y respuestas HTTP para aplicar algo común a todas: token, errores, loading. En el punto de venta uno agrega el token de sesión; además creé un ErrorHandler que registra errores con sucursal y caja.
Si repreguntan

¿Interceptor o ErrorHandler? — El interceptor trabaja sobre el flujo HTTP; el ErrorHandler captura los errores no controlados de la aplicación. En el punto de venta, el interceptor de contexto detecta la respuesta del servidor que indica que la venta perdió su contexto y avisa al cajero; mi ErrorHandler registra el resto, como un error de JavaScript o un módulo que no cargó tras un despliegue.

¿Cómo agregas el token? — Clonando la petición con la cabecera, porque es inmutable: req.clone({ setHeaders: { Authorization: ... } }). En el punto de venta, el interceptor de autenticación lee el token de la cookie de sesión y lo agrega a cada petición; en el backoffice ese interceptor ya es funcional y vive en la librería común.

¿Loading global? — Con un interceptor que cuente peticiones activas y reduzca el contador en finalize, que corre cuando el Observable termina por éxito, por error o al cancelarse. En el punto de venta el spinner se ocultaba solo si la petición salía bien y la caja se quedaba bloqueada cuando fallaba; lo corregí, y la solución de fondo es finalize.

¿Refresh token? — Si la API responde 401 por expiración, el interceptor inicia un único refresh, obtiene el token nuevo y reintenta las peticiones pendientes; si el refresh falla, cierra la sesión. En nuestros proyectos no había refresh token: lo explico como algo que conozco, no como experiencia de producción.

7 · Rendimiento — OnPush, lazy loading, trackBy

¿Cómo funciona OnPush?

Respuesta · sin caso en producción Angular revisa el componente solo cuando cambia la referencia de un input o queda marcado: un evento, un signal, el async pipe o markForCheck(). No lo usamos en producción; lo pondría en tablas de solo lectura.

¿Para qué sirve trackBy o track?

Respuesta Identifica cada elemento para que Angular reutilice los nodos del DOM en vez de recrearlos. En el monitor de pedidos, trackBy por folio conserva las filas que no cambian.

¿Lazy loading o @defer?

Respuesta Lazy loading carga una ruta al entrar; @defer, una parte de la pantalla. En el backoffice, cada área se descarga solo al entrar a su menú.

Una pantalla va lenta, ¿qué haces?

Respuesta Primero mido si el problema es red, JavaScript, change detection o renderizado, y optimizo ahí. Mi buscador usa debounceTime, mínimo de 3 letras y paginación para evitar llamadas innecesarias.

8 · Testing

¿Haces pruebas unitarias?

Respuesta Sí, aislando dependencias con mocks. Probé el guardado de pedidos en sus tres casos — éxito, error de negocio y falla — con Jasmine.
Conviene dominar también

TestBed · spies con jasmine.createSpyObj · HttpTestingController para servicios HTTP · fakeAsync y tick para tiempos como un debounceTime · la diferencia entre prueba unitaria (una pieza aislada) y de integración (varias piezas juntas) · probar Observables suscribiéndote y verificando lo emitido.

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 y aplicaciones en la nube. En producción no, mi backend profesional fue .NET; pero hice una API de inventario de práctica con Quarkus.

¿Cómo haces un endpoint REST en Quarkus?

Respuesta Con @Path en la clase y @GET o @POST en los métodos. En mi API, GET /productos/{id} devuelve uno o 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);               // 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: @ApplicationScoped y @Inject. En mi API, la regla de existencia vive en un servicio inyectado en el endpoint.

¿Cómo accedes a la base de datos?

Respuesta Con Panache, y las escrituras en @Transactional. En mi API, si no alcanza la existencia, no se guarda nada 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 @NotBlank, @Positive y @Valid. Si mando un precio negativo, mi API responde 400 sola.
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 de negocio y un mapeador que las convierte en código HTTP — @ServerExceptionMapper en Quarkus, o el ExceptionMapper estándar. En mi API, vender de más responde 409.
@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, streams y Optional. Mis DTO son records y convierto listas con stream().map().
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 y RestAssured. Mi API tiene seis pruebas: 200, 404, 201, 400, 409 y CORS.
@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 con CORS. En mi API habilité CORS para localhost:4200 y lo probé.
# 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 La 21 es LTS y la 20 no. En mi API usé solo lo estable en la 20.

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, generate, build, test y update. Actualicé el punto de venta de Angular 17 a 19 con ng update, una versión mayor 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? — En CI prefiero npm ci, que instala exacto desde el package-lock.json y es reproducible. El pipeline del punto de venta usaba npm install --legacy-peer-deps; antes de cambiarlo revisaría esos conflictos de dependencias, porque npm ci fallaría con ellos.

¿Cómo manejas ambientes? — Con configuraciones en angular.json que reemplazan el archivo environment al compilar. El punto de venta tiene desarrollo, QA y producción, y cada una trae sus propias URLs de más de 30 servicios del backend.

¿Qué haces si ng update marca problemas? — Voy de una versión mayor a la vez, dejo que corran las migraciones automáticas y corrijo a mano lo demás antes de subir. Así actualicé el punto de venta de 17 a 18 y de 18 a 19; en la segunda se tocaron 204 archivos.

11 · Git

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

Respuesta Una rama por tarea y Pull Request con revisión. En Azure DevOps, cada tarea tenía su rama desde DEV.
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 integra historias sin reescribir commits; rebase mueve tus commits sobre otra base y reescribe su historial, así que evito hacer rebase de commits que otros ya usan. En el punto de venta traía origin/DEV a mi rama con merge antes de cada Pull Request.

¿Cómo resuelves un conflicto? — Traigo la base a mi rama, decido línea por línea — hablando con quien hizo el otro cambio si hace falta — y compilo y pruebo antes del commit. En el punto de venta resolví así un conflicto en la impresión de tickets al traer DEV a mi rama.

¿revert o reset? — revert crea un commit nuevo que deshace otro: es para historial ya compartido. reset mueve la rama y puede reescribir el historial, así que solo lo uso en trabajo local que nadie más tiene.

¿Qué revisas en un Pull Request? — Que haga lo que pide la tarea, que no rompa otra cosa y que maneje errores y estados de carga. En el punto de venta además el PR tenía que pasar SonarQube: cuando bajaba la cobertura o marcaba hallazgos, se corregía antes de entrar a DEV.

¿Convención de commits? — Prefijos que dicen el tipo de cambio — feat:, fix:, refactor:, chore: — y el número de la tarea. Así los uso: por ejemplo, "refactor: revertir integración del botón de reporte en embebido" o "chore: resolve merge conflicts".

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 valueChanges → debounceTime → distinctUntilChanged → switchMap 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 corta + un ejemplo en una frase. Menos de 15 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]."

Al practicar: si una respuesta no te sale, detente, aprende una explicación sencilla y asóciala con una regla fácil de recordar. Mejor eso que memorizar un párrafo.

"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). Ej.: Recepción electrónica: los lotes son signal y un computed habilita "Agregar lote".

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

items.update(xs => [...xs, nuevo]) — arreglo nuevo, nunca push. Ej.: Compra directa: lineasProducto.update(ls => [...ls, nueva]).

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). Ej.: Lo aplicaría en consulta de movimientos: reiniciar filtros al cambiar de producto.

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

toSignal(obs$, { initialValue }) y toObservable(signal). Ej.: Lo aplicaría en mi buscador: el flujo del teclado a signal con toSignal.

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

Input como signal (input.required obligatorio); evento hacia el padre; binding de doble vía. Ej.: Devolución (padre) le pasa la etiqueta al buscador (hijo) por input; el buscador emite el producto elegido por output y la devolución agrega el renglón.

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

provideZonelessChangeDetection(), estable desde Angular 20.2. Ej.: El backoffice usa Zone con eventCoalescing; zoneless sería el siguiente paso.

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

switchMap: se desuscribe de la búsqueda anterior (con HttpClient, la aborta). Antes: debounceTime y distinctUntilChanged. Ej.: Mi buscador espera 500 ms; con switchMap la búsqueda vieja se abortaría.

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

exhaustMap: ignora clics mientras la petición sigue en curso. Ej.: Doble clic en "Cobrar" del punto de venta: mi directiva lo bloquea; hoy, exhaustMap.

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

concatMap: los pone en cola. Ej.: Lo aplicaría en guardados que deben ir en orden, como líneas de un pedido.

RxJS · ¿forkJoin o combineLatest?
Ver respuesta

forkJoin emite una vez cuando todos completan (si uno falla sin manejar, falla todo); combineLatest emite cada vez que cualquiera cambia. Ej.: Subrogación: siete consultas del producto a la vez con forkJoin.

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

async pipe / toSignal; takeUntilDestroyed() al final del pipe; takeUntil(destroy$) en código anterior. Ej.: Mi buscador se cierra con takeUntil(destroy$) al destruirse.

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

Depende: dentro de cada petición si aceptas resultados parciales; afuera si un fallo invalida todo. Ej.: Subrogación: va afuera del forkJoin, porque si falla una consulta el producto no se agrega.

RxJS · ¿Subject vs BehaviorSubject?
Ver respuesta

Subject no guarda valor (eventos); BehaviorSubject guarda el último y necesita valor inicial (estado). Ej.: El cliente de la venta está en un BehaviorSubject: lealtad lo recibe al abrirse.

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

null. Si no, un objeto con el error: { rfc: true }. Ej.: Apertura de caja: shouldBeLess no deja pasar del fondo máximo.

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

En el FormGroup: fb.group({...}, { validators: rangoFechas('desde','hasta') }). Ej.: Apertura de caja: el validador va en el grupo del formulario.

Forms · ¿value o getRawValue?
Ver respuesta

value excluye deshabilitados; getRawValue trae todo. Ej.: Pedido manual: el proveedor deshabilitado se lee con getRawValue.

Forms · ¿setValue o patchValue?
Ver respuesta

setValue exige todos los campos; patchValue acepta parcial. Ej.: Alta de cliente: patchValue limpia solo la fecha de nacimiento inválida.

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

formArrayName="lineas" y dentro [formGroupName]="i" por cada control. Ej.: Alta de cliente: un renglón por teléfono.

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

updateOn: 'blur' en el control. Ej.: Lo aplicaría al validar contra el servidor que un proveedor exista.

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

writeValue, registerOnChange, registerOnTouched, setDisabledState + provider NG_VALUE_ACCESSOR con multi: true. Ej.: Sería la mejora de mi buscador para usarlo con formControlName.

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. Ej.: Lo aplicaría en una tarjeta genérica: título, cuerpo y acciones.

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. Ej.: Punto de venta: los mismos componentes de botones de pago en todas las pantallas.

Reutilizables · ¿Directiva o componente?
Ver respuesta

Directiva agrega comportamiento a un elemento existente; componente tiene plantilla propia. Ej.: Mi directiva contra el doble clic agrega comportamiento a cualquier botón.

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

provideHttpClient(withInterceptors([auth, loading, error])) en app.config.ts. Ej.: El interceptor de autenticación del backoffice es funcional y vive en la librería.

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

next(req.clone({ setHeaders: { Authorization: 'Bearer ' + t } })) — la petición es inmutable. Ej.: Punto de venta: el token de la cookie se agrega a cada petición.

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. Ej.: Lección del spinner que bloqueaba la caja cuando la petición fallaba.

HTTP · ¿Interceptor o ErrorHandler?
Ver respuesta

Interceptor: errores HTTP, reacciona por petición. ErrorHandler: cualquier error no controlado, para registrarlo. Ej.: Contexto de venta perdido → interceptor; error de JavaScript → mi ErrorHandler.

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

Nueva referencia en un input, o marcado por un evento, un signal, el async pipe o markForCheck. Ej.: Lo aplicaría en las tablas de los monitores, que solo muestran datos.

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. Ej.: Monitor de pedidos: trackBy por folio.

Rendimiento · ¿Lazy loading vs @defer?
Ver respuesta

Lazy separa por ruta (loadChildren/loadComponent); @defer separa partes de una pantalla (on viewport, interaction…). Ej.: Backoffice: cada área es un microfrontend que se descarga al entrar.

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

Se ejecutan en cada ciclo; usa pipe puro o computed. Ej.: Lo cambiaría por un computed o un pipe puro.

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. Ej.: Punto de venta: core con el guard, shared con los modales, features lazy.

Arquitectura · ¿Regla de dependencias?
Ver respuesta

Feature usa Shared y Core; nunca importa de otro feature. Lo compartido se sube a Shared. Ej.: Backoffice: lo compartido se sube a la librería común.

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

En providers de la ruta del feature: vive mientras estás en él. Ej.: Recepción electrónica: los lotes viven en el componente que los captura.

Java · ¿Java 20 es LTS?
Ver respuesta

No. La LTS es 21. En 20, virtual threads y pattern matching en switch eran preview. Ej.: En mi API usé solo lo estable en Java 20.

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. Ej.: Mi API: GET /productos/{id} devuelve uno o 404.

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

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

Quarkus · ¿Qué te da PanacheEntity?
Ver respuesta

id, persist(), findById(), findByIdOptional(), listAll(), list() y find(). Escrituras en @Transactional. Ej.: Mi API: descontar existencia es @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. Ej.: Mi API: precio negativo → 400.

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

Un método con @ServerExceptionMapper que devuelve RestResponse.status(CONFLICT, ...). Ej.: Mi API: vender de más → 409 "hay 12, pides 999".

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). Ej.: Mi API: seis pruebas, del 200 al permiso de CORS.

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

lista.stream().map(ProductoDto::from).toList() — como un Select de LINQ. Ej.: Mi API: entidades a ProductoDto con stream.

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

provideHttpClient() + provideHttpClientTesting(), HttpTestingController.expectOne, flush, verify. Ej.: Pedido manual: el servicio de guardar probado en éxito, error y excepción.

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. Ej.: Todos los días con ng generate.

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. Ej.: Punto de venta: build por ambiente y actualización de 17 a 19.

npm · ¿npm install o npm ci?
Ver respuesta

ci instala exacto desde el lockfile y es reproducible: para pipelines. Ej.: El pipeline usaba npm install --legacy-peer-deps; antes de pasar a npm ci revisaría esos conflictos.

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. Ej.: Traía origin/DEV a mi rama con merge antes del PR.

Git · ¿revert o reset?
Ver respuesta

revert para lo que ya está en el remoto (crea un commit nuevo); reset solo para commits locales. Ej.: revert para historial compartido; reset solo para trabajo local.

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

git stash y después git stash pop. Ej.: Al cambiar de tarea con trabajo a medias.

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

git cherry-pick <hash>. Ej.: Para llevar un fix puntual a otra rama.

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