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ó.
25 min de enfoque → 5 de descanso. Cada 4 pomodoros, descanso de 20. Sigue corriendo arriba a la derecha aunque cambies de pestaña.
| Minuto | Qué haces |
|---|---|
| 0 – 2 | Escribe en una línea el objetivo: "Teclear el buscador con switchMap sin mirar". Un objetivo por pomodoro. |
| 2 – 20 | Trabajo activo: teclear un ejercicio o responder en voz alta. Nada de solo leer. |
| 20 – 25 | Hoja 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. |
| Descanso | Lejos de la pantalla: párate, agua, camina. No redes sociales (el cerebro no descansa). |
| Técnica | Por qué funciona | Cómo la aplicas aquí |
|---|---|---|
| Recuperación activa | Recordar 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 espaciada | Repasar 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ón | Lo 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 Feynman | Si 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. |
| Intercalado | Mezclar 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 grabado | Oírte revela muletillas y respuestas largas. | Graba el simulacro con el celular, escúchalo y rehace las 3 peores respuestas. |
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.
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.
Una hora antes, nada nuevo. En Java ya puedes hablar de tu proyecto, si lo corriste y lo leíste.
Desktop\farmacia-api como referencia. Tarjetas "Repasar".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.
¿Cómo estructuras un proyecto Angular?
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
¿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.
¿Qué es un signal y por qué lo usas?
computed habilita "Agregar lote" solo si hay lote, piezas y caducidad.¿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?
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>
¿Observable o Promesa?
¿switchMap, mergeMap, concatMap o exhaustMap?
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.¿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.
¿Cómo haces varias peticiones a la vez?
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?
takeUntilDestroyed. Mi buscador deja de escuchar el teclado al destruirse la pantalla.¿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.
¿Por qué Reactive Forms?
¿Cómo haces un validador personalizado?
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?
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?
FormArray, agregando o quitando renglones. En el alta de cliente, cada teléfono es un renglón.¿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.
¿Qué hace reutilizable a un componente?
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?
formControlName. No lo he usado; se lo pondría a mi buscador para usarlo como un campo más del formulario.¿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?
¿Qué es un interceptor y para qué lo usas?
ErrorHandler que registra errores con sucursal y caja.¿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.
¿Cómo funciona OnPush?
async pipe o markForCheck(). No lo usamos en producción; lo pondría en tablas de solo lectura.¿Para qué sirve trackBy o track?
trackBy por folio conserva las filas que no cambian.¿Lazy loading o @defer?
@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?
debounceTime, mínimo de 3 letras y paginación para evitar llamadas innecesarias.¿Haces pruebas unitarias?
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.
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:
Ú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?
¿Cómo haces un endpoint REST en Quarkus?
@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?
@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?
@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?
@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?
@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?
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?
@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?
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?
Pregunta que puedes hacer tú: "¿Tienen planeado pasar a Java 21, que es la LTS?"
| .NET | Java / 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 · SaveChanges | Hibernate Panache · persist() + @Transactional |
Data Annotations [Required] | Bean Validation @NotBlank |
| Middleware de excepciones | @ServerExceptionMapper |
LINQ Select / Where | Streams map / filter |
appsettings.{Env}.json | application.properties con %dev. / %prod. |
xUnit + WebApplicationFactory | JUnit 5 + @QuarkusTest + RestAssured |
| Swashbuckle / Swagger | SmallRye OpenAPI (/q/swagger-ui) |
dotnet watch | ./mvnw quarkus:dev |
NuGet · .csproj | Maven · pom.xml |
¿Qué comandos del CLI usas en el día a día?
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.| Comando | Para qué |
|---|---|
ng new mi-app --style=scss --routing | Crear proyecto |
ng serve -o | Servidor de desarrollo y abrir el navegador |
ng g c features/pedidos/lista | Componente (standalone por default) |
ng g s core/services/sesion | Servicio |
ng g guard core/guards/auth · ng g interceptor core/interceptors/auth | Guard e interceptor (funcionales) |
ng g pipe · ng g directive · ng g library ui | Pipe, directiva, librería |
ng build --configuration production | Compilar para un ambiente |
ng test --code-coverage | Pruebas con reporte de cobertura |
ng add @angular/material | Instalar y configurar una librería |
ng update @angular/core @angular/cli | Actualizar de versión con migraciones automáticas |
npm install · npm ci | Instalar dependencias · instalar exacto desde el lockfile (en pipelines) |
¿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.
¿Cómo es tu flujo de trabajo con Git?
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
| Comando | Para qué |
|---|---|
git status · git log --oneline --graph | Ver estado e historial |
git switch -c rama | Crear y cambiar de rama |
git pull = fetch + merge | Traer y combinar cambios del remoto |
git stash · git stash pop | Guardar 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~1 | Deshacer el último commit local sin perder los cambios |
git diff · git blame archivo | Ver diferencias · quién cambió cada línea |
¿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".
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
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')); }
}
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.
@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');
}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.
Si el producto ya está, usa map para devolver una línea nueva con cantidad + 1; si no, [...ls, { ...p, cantidad: 1 }].
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>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.
@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)" />
}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(--...).
// 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>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.
Los 4 métodos: entra (writeValue), sale (registerOnChange), tocado (registerOnTouched), deshabilitar (setDisabledState).
@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();
}
}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.
El estado de carga: tap antes de la petición y finalize dentro del switchMap.
@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[] });
}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.
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).
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.
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> }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().
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>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).
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()]]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.
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>En un grupo con desde y hasta, error rango si "desde" es mayor que "hasta". El mensaje aparece debajo del grupo.
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> }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).
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]))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.
// 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 },
];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.
@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.
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).mvnw.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.http://localhost:8080/q/dev-ui. Si ves la Dev UI, estás listo.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.
// 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(); }
}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.
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); }
}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).
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();
}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.
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()));
}
}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.
@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.ContentTypeHabilita 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.
# 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.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.
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."
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.
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.
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.
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?
| Pregunta | Qué 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 CDMX | Decide 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?" |
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.
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".
items.update(xs => [...xs, nuevo]) — arreglo nuevo, nunca push. Ej.: Compra directa: lineasProducto.update(ls => [...ls, nueva]).
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.
toSignal(obs$, { initialValue }) y toObservable(signal). Ej.: Lo aplicaría en mi buscador: el flujo del teclado a signal con toSignal.
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.
provideZonelessChangeDetection(), estable desde Angular 20.2. Ej.: El backoffice usa Zone con eventCoalescing; zoneless sería el siguiente paso.
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.
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.
concatMap: los pone en cola. Ej.: Lo aplicaría en guardados que deben ir en orden, como líneas de un pedido.
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.
async pipe / toSignal; takeUntilDestroyed() al final del pipe; takeUntil(destroy$) en código anterior. Ej.: Mi buscador se cierra con takeUntil(destroy$) al destruirse.
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.
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.
null. Si no, un objeto con el error: { rfc: true }. Ej.: Apertura de caja: shouldBeLess no deja pasar del fondo máximo.
En el FormGroup: fb.group({...}, { validators: rangoFechas('desde','hasta') }). Ej.: Apertura de caja: el validador va en el grupo del formulario.
value excluye deshabilitados; getRawValue trae todo. Ej.: Pedido manual: el proveedor deshabilitado se lee con getRawValue.
setValue exige todos los campos; patchValue acepta parcial. Ej.: Alta de cliente: patchValue limpia solo la fecha de nacimiento inválida.
formArrayName="lineas" y dentro [formGroupName]="i" por cada control. Ej.: Alta de cliente: un renglón por teléfono.
updateOn: 'blur' en el control. Ej.: Lo aplicaría al validar contra el servidor que un proveedor exista.
writeValue, registerOnChange, registerOnTouched, setDisabledState + provider NG_VALUE_ACCESSOR con multi: true. Ej.: Sería la mejora de mi buscador para usarlo con formControlName.
<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.
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.
Directiva agrega comportamiento a un elemento existente; componente tiene plantilla propia. Ej.: Mi directiva contra el doble clic agrega comportamiento a cualquier botón.
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.
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.
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.
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.
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.
Identificar cada elemento para no recrear el DOM al llegar una lista nueva. Obligatorio en @for. Ej.: Monitor de pedidos: trackBy por folio.
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.
Se ejecutan en cada ciclo; usa pipe puro o computed. Ej.: Lo cambiaría por un computed o un pipe puro.
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.
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.
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.
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.
@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.
El servicio con @ApplicationScoped; en el resource, @Inject InventarioService inventario; (CDI, resuelto al compilar). Ej.: Mi API: InventarioService inyectado en el resource.
id, persist(), findById(), findByIdOptional(), listAll(), list() y find(). Escrituras en @Transactional. Ej.: Mi API: descontar existencia es @Transactional.
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.
Un método con @ServerExceptionMapper que devuelve RestResponse.status(CONFLICT, ...). Ej.: Mi API: vender de más → 409 "hay 12, pides 999".
/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.
lista.stream().map(ProductoDto::from).toList() — como un Select de LINQ. Ej.: Mi API: entidades a ProductoDto con stream.
provideHttpClient() + provideHttpClientTesting(), HttpTestingController.expectOne, flush, verify. Ej.: Pedido manual: el servicio de guardar probado en éxito, error y excepción.
ng g c ruta/nombre · ng g s ruta/nombre · ng g interceptor ruta/nombre. Ej.: Todos los días con ng generate.
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.
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.
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.
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 stash y después git stash pop. Ej.: Al cambiar de tarea con trabajo a medias.
git cherry-pick <hash>. Ej.: Para llevar un fix puntual a otra rama.
practica abierto por si piden compartir pantalla.