Capgemini es una consultora grande: te van a entrevistar primero la reclutadora (encaje, salario, modalidad), después un líder técnico (hablado + a veces un ejercicio en vivo) y muchas veces el cliente final. La lista de "imprescindibles" es casi un temario: cada punto es una pregunta segura. Esta biblia sigue ese orden.
| Requisito | Pestaña | Tu ancla real |
|---|---|---|
| Angular (v20+ preferente) | Todas | Migraste el backoffice a Angular 20.3 con Native Federation, e hiciste las actualizaciones del POS de 17→18 y 18→19.2 (204 archivos). Estás justo en la versión que piden |
| Arquitectura modular (Core, Shared, Feature) | Arquitectura modular | El punto de venta está hecho literalmente así: core/ (servicios, interceptores, guards), shared/ (componentes + 25 modales de negocio) y 14 features lazy (tienda, pago, entrega, lealtad…). El backoffice lo lleva a microfrontends: host + 16 módulos (13 remotos en el manifiesto) + librería |
| Signals para estado en componentes | Signals | Inventarios y recepción electrónica: signal() + computed() (puedeAgregarLote) |
| RxJS para flujos asíncronos y eventos | RxJS | Buscador de productos compartido: Subject + debounceTime(500) + takeUntil, paginación por hash |
| Reactive Forms | Reactive Forms | Todo el repo: más de 1,200 FormControl, 570 FormBuilder, validadores required/pattern/email/compose |
| Componentes reutilizables y desacoplados | Componentes reutilizables | La librería común del backoffice (216 de sus 328 commits son tuyos) y tu directiva anti doble clic, usada en 23 botones de pago y cancelación del POS |
| APIs REST e interceptores HTTP | HTTP e interceptores | El POS tiene 4 interceptores HTTP (auth, timeout, logging, contexto) que escribieron tus compañeros y tú mantuviste; tú creaste el GlobalErrorHandler que registra cada error de las terminales con sucursal, caja y línea |
| Rendimiento (OnPush, lazy loading, trackBy) | Rendimiento | OnPush en ~32 componentes, cada remoto se carga bajo demanda |
| Deseable: Java 21 (la reclutadora dijo 20), Quarkus | Java 20 · Quarkus | No lo usas. Tu backend es .NET — traza el paralelo con honestidad |
| Requisito | Tú | Cómo lo manejas |
|---|---|---|
| 4 años de experiencia | ✅ 5+ | Enero 2021 a agosto 2026, menos el hueco de ago–dic 2021 ≈ 5 años. Di siempre "más de cinco años" en total y "más de cuatro años" en la cadena farmacéutica |
| Stack Angular completo | ✅ Fuerte | Es exactamente tu día a día. La vacante está hecha para tu perfil técnico |
| Inglés técnico | ✅ Alcanza | "Técnico" = leer documentación y escribir commits/tickets. Lo cumples. No prometas conversación |
| Java 20 / Quarkus | 🟡 No | Es deseable, no imprescindible. Pestaña Java: 5 minutos de conceptos para no quedarte en blanco |
| Licenciatura concluida | 🟡 Pasante | Dilo tú antes de que salga en el estudio socioeconómico: "Soy pasante de Ingeniería en TIC por el Tecnológico de Tlalnepantla; la titulación está en trámite." En consultoras suele bastar con carta de pasante o historial. Pregunta si es requisito del cliente |
| Híbrido CDMX | ❌ Choca | Tu perfil es solo remoto y vives en Culiacán. Decide antes de la llamada (card de abajo) |
| Horario 8:30–18:00 | 🟡 Largo | Es horario de cliente. Anótalo como dato para la negociación, no como problema |
La vacante es híbrida en CDMX. Es la primera pregunta de filtro que te van a hacer ("¿cuántos días puedes ir a oficina?"). Tienes tres respuestas honestas; escoge una y sostenla:
❌ Lo que no: decir que sí y luego pedir remoto el primer día. Se pierde la confianza de la reclutadora, que es quien te propone para las siguientes vacantes.
toSignal/toObservable). Pestañas Signals y RxJS.FormArray, validación cruzada, validador asíncrono. Pestaña Forms.Lección de tu última entrevista
Ensayaste lo complejo y te pidieron lo simple pero tecleado: un buscador de productos completo, con una restricción rara ("sin HttpClient"). Aquí no te va a pasar: cada tema tiene su parte hablada y su ejercicio escrito, y la regla es la misma — pregunta la restricción antes de teclear y resuelve con una abstracción.
Las tres reglas de tu ex jefe. Un Semi Sr se distingue de un Jr por cómo explica, no por cuántas APIs recita.
Cero "creo", "siento", "me parece", "más o menos". Si no sabes algo, dilo con firmeza: "No lo he usado en producción. Lo que sí conozco es X, y el equivalente sería Y."
Cada respuesta situada: dónde lo usaste, qué problema resolvía, qué decidiste. "En el backoffice de un punto de venta con 86 sucursales…"
"Change detection", "zoneless", "glitch-free", "higher-order observable", "ControlValueAccessor", "content projection", "code splitting". Un término preciso vale por años de experiencia.
La vacante dice "Core, Shared, Feature Modules", pero pide Angular 20, donde lo estándar ya son componentes standalone. La respuesta que te distingue: la idea de las tres capas sigue viva; lo que cambió es cómo se implementa.
¿Cómo organizas una aplicación Angular grande?
core los interceptores, el guard de sesión y los servicios; en shared los componentes y unos 25 modales de negocio — búsqueda de medicamento, pago, tarjeta de lealtad —; y catorce features cargados por lazy loading: tienda, pago, entrega, lealtad. Y en el backoffice lo llevamos al extremo: el shell es el Core, una librería común es el Shared, y cada feature es un microfrontend independiente con Native Federation."| Punto de venta (Angular 19.2) | Backoffice (Angular 20.3) | |
|---|---|---|
| Core | core/: 4 interceptores HTTP (auth, timeout, logging, contexto) + el GlobalErrorHandler, sessionGuardFn, servicios | host/: shell, rutas, sidebar, manifiesto de federation (13 remotos) |
| Shared | shared/ + catalogo/ (botones sw-button) + directives/ | common/tools/projects/libreria, compartida en federation |
| Feature | 14 NgModules lazy dentro de LayoutComponent | 16 módulos (13 publicados como remotos en el manifiesto), cada uno con su build y su deploy |
| Estado | Servicios + Observables | Signals-first en lo nuevo (signal/computed/inject) |
Cómo lo cuentas: "Conozco las dos versiones del mismo patrón: la clásica, con NgModules y lazy loading dentro de una sola app, y la distribuida, con microfrontends standalone. Sé cuándo conviene cada una."
| Capa | Qué va | Qué NO va | En Angular 20 (standalone) |
|---|---|---|---|
| Core | Interceptores, guards, AuthService, manejo global de errores, layout/shell, config | Componentes que usan los features | Providers en app.config.ts (provideHttpClient(withInterceptors(...)), provideRouter). Ya no hay CoreModule con guard anti doble import: el singleton lo da providedIn: 'root' |
| Shared | Botones, inputs, tablas, modales, pipes, directivas, validadores, modelos comunes | Servicios con estado de negocio, llamadas HTTP de un feature | Cada pieza es standalone y se importa donde se usa. Ya no hay un SharedModule gigante que arrastra todo a todos los bundles |
| Feature | Pantallas de un área (pedidos, inventario), sus servicios y su estado | Imports desde otro feature | loadChildren: () => import('./features/pedidos/pedidos.routes') con su propio arreglo de rutas y providers a nivel de ruta |
src/app/
core/
interceptors/ auth.interceptor.ts · error.interceptor.ts
guards/ auth.guard.ts
services/ session.service.ts · notification.service.ts
layout/ shell.component.ts
shared/
ui/ button/ · data-table/ · form-field/ · modal/
pipes/ currency-mx.pipe.ts
directives/ autofocus.directive.ts
validators/ rfc.validator.ts
models/ paged-result.ts
features/
pedidos/
pedidos.routes.ts ← lazy
data/ pedidos.api.ts ← HTTP del feature
state/ pedidos.store.ts ← signals
pages/ lista-pedidos/ · detalle-pedido/
ui/ pedido-card/ ← presentacionales del feature
app.config.ts · app.routes.ts
// app.routes.ts
export const routes: Routes = [
{ path: '', component: ShellComponent, canActivate: [authGuard], children: [
{ path: 'pedidos', loadChildren: () => import('./features/pedidos/pedidos.routes')
.then(m => m.PEDIDOS_ROUTES) },
{ path: 'inventario', loadComponent: () => import('./features/inventario/inventario.page')
.then(m => m.InventarioPage) },
]},
{ path: '**', redirectTo: '' }
];
// features/pedidos/pedidos.routes.ts
export const PEDIDOS_ROUTES: Routes = [
{ path: '', providers: [PedidosStore], // estado vive mientras vives en el feature
children: [
{ path: '', component: ListaPedidosPage },
{ path: ':id', component: DetallePedidoPage, resolve: { pedido: pedidoResolver } },
]}
];
ng g @angular/core:standalone, por pasos. Lo importante no es el NgModule, es la frontera: qué expone cada capa y quién puede importar a quién."Regla de lint. En Nx: @nx/enforce-module-boundaries con tags (type:feature solo depende de type:ui, type:data-access, type:util). Sin Nx: eslint-plugin-boundaries o path aliases (@shared/*, @core/*) + revisión en PR. Si dos features necesitan lo mismo, eso se sube a Shared (o a una librería), no se importa de lado.
input() y emite output(), es OnPush y no inyecta servicios de negocio. Así el presentacional se reusa y se testea sin mocks de HTTP."Tres niveles: local del componente (signal dentro del componente), del feature (servicio con signals provisto en la ruta del feature → muere al salir), global (providedIn: 'root': sesión, usuario, permisos). NgRx/SignalStore solo si hay mucho estado compartido con efectos complejos; en el backoffice no lo usamos — servicios con signals bastaban.
Es la pregunta obligada de Angular 20. Ten claro: qué es, las 5 primitivas, cuándo no usar effect, y cómo convive con RxJS.
¿Qué es un signal y por qué existe?
computed dependen de él, y solo actualiza eso. Antes, con Zone.js, cualquier evento disparaba change detection sobre todo el árbol. Con signals la reactividad es fina y explícita, y abre la puerta a apps zoneless. En inventarios lo usé así: la lista de lotes es un signal, y si se puede agregar un lote es un computed que depende de la lista y del formulario — no hay suscripciones que limpiar."| API | Para qué | Ejemplo |
|---|---|---|
signal(v) | Estado escribible | items = signal<Item[]>([]) · .set() · .update(fn) |
computed(fn) | Derivado, memoizado, lazy, solo lectura | total = computed(() => this.items().reduce(...)) |
effect(fn) | Efecto secundario hacia fuera de Angular | Guardar en localStorage, log, pintar un canvas |
linkedSignal | Escribible que se resetea cuando cambia su fuente | Página actual que vuelve a 1 cuando cambia el filtro |
input() / input.required() | Inputs como signal | producto = input.required<Producto>() |
output() | Evento hacia el padre (no es signal) | seleccionado = output<Producto>() |
model() | Two-way binding con signal | valor = model(0) → [(valor)]="x" |
viewChild() / contentChildren() | Queries como signal | input = viewChild.required<ElementRef>('q') |
resource / httpResource | Carga asíncrona como signal (valor, loading, error) | Experimental en v19–20; menciónalo, no lo vendas como estándar |
@Component({
selector: 'app-carrito',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (l of lineas(); track l.sku) {
<app-linea [linea]="l" (quitar)="quitar(l.sku)" />
} @empty { <p>Carrito vacío</p> }
<p>Total: {{ total() | currency:'MXN' }}</p>
<button [disabled]="!puedeCobrar()">Cobrar</button>`
})
export class CarritoComponent {
lineas = signal<Linea[]>([]);
total = computed(() => this.lineas().reduce((s, l) => s + l.precio * l.cantidad, 0));
puedeCobrar = computed(() => this.lineas().length > 0 && this.total() > 0);
agregar(l: Linea) { this.lineas.update(ls => [...ls, l]); } // inmutable
quitar(sku: string) { this.lineas.update(ls => ls.filter(x => x.sku !== sku)); }
}
this.lineas().push(l). Mutas el arreglo, la referencia no cambia y el signal no notifica. Siempre update con un arreglo nuevo.effect es para sincronizar con algo que no es Angular: localStorage, una librería de gráficas, analítica. Si lo uso para escribir en otro signal, es olor: eso es un computed o un linkedSignal. Un effect que copia estado genera ciclos y renders de más."toSignal para llevar el resultado a la plantilla."private q = signal('');
resultados = toSignal(
toObservable(this.q).pipe(
debounceTime(300), distinctUntilChanged(),
switchMap(q => q.length < 3 ? of([]) : this.api.buscar(q))
), { initialValue: [] as Producto[] }
);
Un computed nunca ve un estado intermedio inconsistente: si a y b cambian, el derivado se recalcula una vez con los dos valores nuevos, no con uno viejo y uno nuevo. Además es lazy (no calcula hasta que alguien lo lee) y memoizado (si las dependencias no cambian, no recalcula).
Quitar Zone.js: provideZonelessChangeDetection() (estable en Angular 20.2). Change detection se dispara por signals, eventos de plantilla, async pipe y markForCheck — no por cualquier setTimeout. Gana bundle más chico y menos ciclos. Requisito: componentes compatibles con OnPush (estado en signals o observables con async).
@Input() x!: T → x = input.required<T>(); en la plantilla x → x(); ngOnChanges que recalculaba algo → computed. Hay schematic: ng g @angular/core:signal-input-migration (y los de output y queries).
¿Observable vs Promesa?
unsubscribe. En el backoffice conviven los dos: los flujos de un solo disparo, como guardar un pedido, los modelamos con promesas y await; los flujos continuos, como el buscador de productos, con RxJS."| Operador | Si llega uno nuevo mientras el anterior sigue… | Úsalo en |
|---|---|---|
switchMap | Cancela el anterior | Búsqueda, autocompletar, cambio de filtro |
concatMap | Lo encola, en orden | Guardados que deben ir en secuencia |
mergeMap | Corren en paralelo | Subir varios archivos independientes |
exhaustMap | Lo ignora hasta que termine | Botón "Cobrar" / login: evita doble envío |
throttleTime, que bloquea 800 ms. Si lo hiciera hoy usaría exhaustMap, porque bloquea mientras la petición siga viva y no por un tiempo fijo."switchMap. ¿escribir en orden? → concatMap. ¿escribir y que no se repita? → exhaustMap. ¿muchos a la vez? → mergeMap.| Operador | Emite | Caso |
|---|---|---|
forkJoin | Una vez, cuando todos completan | Cargar catálogos al abrir una pantalla |
combineLatest | Cada vez que cualquiera emite (tras el primero de todos) | Filtros + página + orden → recargar |
withLatestFrom | Solo cuando emite el principal, con el último del otro | Clic en guardar + estado actual del form |
merge | Todo lo de todos, intercalado | Varios eventos que disparan lo mismo |
| Tipo | Al suscribirte recibes | Caso |
|---|---|---|
Subject | Nada del pasado | Eventos: clic, teclazo |
BehaviorSubject | El último valor (requiere inicial) | Estado (hoy casi siempre lo reemplaza un signal) |
ReplaySubject(n) | Los últimos n | Caché de las últimas respuestas |
async pipe o toSignal — se desuscriben solos. Primera opción.takeUntilDestroyed() (Angular 16+) — en contexto de inyección, o pasándole DestroyRef.takeUntil(this.destroy$) + ngOnDestroy. Es el que tiene el buscador compartido del backoffice.export class MonitorComponent {
private api = inject(PedidosApi);
constructor() {
interval(30_000).pipe(
startWith(0),
switchMap(() => this.api.pendientes()),
takeUntilDestroyed()
).subscribe(p => this.pendientes.set(p));
}
pendientes = signal<Pedido[]>([]);
}
takeUntilDestroyed va al final del pipe. Si lo pones antes de un switchMap, el interno sigue vivo.this.api.buscar(q).pipe(
retry({ count: 2, delay: 1000 }), // reintenta solo errores transitorios
catchError(err => { // DENTRO del switchMap: el flujo externo sobrevive
this.error.set('No se pudo buscar');
return of([]);
})
)
catchError está afuera, el primer error mata el buscador y los siguientes teclazos ya no hacen nada. Dentro del switchMap, solo muere esa petición y el flujo de teclazos sigue vivo."Cold: cada suscriptor dispara su propia ejecución (HTTP: dos subscribe = dos peticiones). Hot: la fuente existe aparte y los suscriptores la comparten (fromEvent, un Subject). Para compartir una petición: shareReplay({ bufferSize: 1, refCount: true }).
fromEvent<InputEvent>(this.input().nativeElement, 'input').pipe(
map(e => (e.target as HTMLInputElement).value.trim()),
debounceTime(300), distinctUntilChanged(), filter(q => q.length >= 3),
switchMap(q => this.api.buscar(q).pipe(catchError(() => of([])))),
takeUntilDestroyed(this.destroyRef)
).subscribe(r => this.resultados.set(r));
Aunque en la práctica prefieres FormControl.valueChanges — ya tienes el valor sin tocar el DOM.
Aquí tienes ventaja real: el backoffice usa Reactive Forms en todos lados. Lo que un Semi Sr debe mostrar: formularios tipados, validadores propios, validación cruzada, asíncrona y FormArray.
¿Template-driven vs Reactive?
ngModel; sirve para formularios de dos campos. En el backoffice todo es reactive porque las capturas son largas — pedidos, recepción de mercancía — con validaciones que dependen de otros campos."type LineaForm = FormGroup<{
sku: FormControl<string>;
cantidad: FormControl<number>;
}>;
@Component({ /* ... */ imports: [ReactiveFormsModule] })
export class PedidoFormComponent {
private fb = inject(NonNullableFormBuilder); // no-nullable: reset() vuelve al inicial, no a null
private api = inject(ProveedoresApi);
form = this.fb.group({
proveedor: this.fb.control('', {
validators: [Validators.required],
asyncValidators: [proveedorExiste(this.api)],
updateOn: 'blur' // el async no pega al servidor en cada tecla
}),
rfc: ['', [Validators.required, rfcValidator()]],
fechaEntrega: ['', Validators.required],
fechaLimite: ['', Validators.required],
lineas: this.fb.array<LineaForm>([], Validators.minLength(1)),
}, { validators: rangoFechas('fechaEntrega', 'fechaLimite') });
get lineas() { return this.form.controls.lineas; }
agregarLinea() {
this.lineas.push(this.fb.group({
sku: ['', Validators.required],
cantidad: [1, [Validators.required, Validators.min(1)]],
}));
}
quitarLinea(i: number) { this.lineas.removeAt(i); }
guardar() {
if (this.form.invalid) { this.form.markAllAsTouched(); return; }
const pedido = this.form.getRawValue(); // tipado; incluye campos deshabilitados
// ...
}
}
<form [formGroup]="form" (ngSubmit)="guardar()">
<input formControlName="rfc" />
@if (form.controls.rfc.touched && form.controls.rfc.hasError('rfc')) {
<small>RFC inválido</small>
}
@if (form.hasError('rangoFechas')) { <small>La entrega debe ser antes del límite</small> }
<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)="quitarLinea(i)">Quitar</button>
</div>
}
</div>
<button type="button" (click)="agregarLinea()">Agregar línea</button>
<button type="submit" [disabled]="form.pending">Guardar</button>
</form>
// Síncrono, de un control
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(c.value.toUpperCase()) ? null : { rfc: true };
}
// Cruzado, a nivel de grupo
export function rangoFechas(desde: string, hasta: string): ValidatorFn {
return (g: AbstractControl) => {
const a = g.get(desde)?.value, b = g.get(hasta)?.value;
return a && b && a > b ? { rangoFechas: true } : null;
};
}
// Asíncrono
export function proveedorExiste(api: ProveedoresApi): AsyncValidatorFn {
return (c) => !c.value ? of(null) : api.existe(c.value).pipe(
map(ok => ok ? null : { proveedorNoExiste: true }),
catchError(() => of(null)) // si el servicio falla, no bloquees el form
);
}
null = válido (no false). Validador vacío devuelve null y deja a required encargarse. El async solo corre si los síncronos pasan. form.pending mientras corre.// Si el tipo de pago es tarjeta, habilita "referencia" y hazla obligatoria
this.form.controls.tipoPago.valueChanges.pipe(takeUntilDestroyed()).subscribe(tipo => {
const ref = this.form.controls.referencia;
if (tipo === 'tarjeta') { ref.enable(); ref.addValidators(Validators.required); }
else { ref.disable(); ref.removeValidators(Validators.required); ref.reset(); }
ref.updateValueAndValidity();
});
O a signals: tipoPago = toSignal(this.form.controls.tipoPago.valueChanges, { initialValue: ... }).
value excluye los controles deshabilitados (y en forms no tipados todo es Partial). getRawValue() trae todo. Para enviar al backend casi siempre getRawValue().
setValue exige la estructura completa (falla si falta una llave). patchValue acepta parcial. Para cargar un registro para edición: patchValue. Con { emitEvent: false } no disparas valueChanges.
Implementando ControlValueAccessor. Está resuelto en la pestaña Componentes reutilizables y en el ejercicio E3.
Es la API nueva basada en signals (experimental a partir de Angular 21). Menciónalo como algo que estás siguiendo; en producción hoy se usan Reactive Forms.
Es la "actividad a desempeñar" principal: componentes reutilizables y sistemas de UI escalables, mantenibilidad y consistencia del diseño. Van a querer ver que sabes hacer una pieza que otros usan sin leer su código.
¿Qué hace reutilizable a un componente?
input() para datos, output() para eventos, y nada de inyectar servicios de negocio. Dos, content projection para que el que lo usa ponga su contenido sin que yo prevea cada caso. Tres, se integra con formularios implementando ControlValueAccessor. Y cuatro, el estilo sale de tokens, variables CSS, no de colores escritos a mano. En el backoffice el buscador de productos era así: lo usaban varios módulos, recibía configuración, emitía el producto elegido y no sabía nada del pedido que lo usaba."@Component({
selector: 'ui-button',
changeDetection: ChangeDetectionStrategy.OnPush,
host: { '[class]': '"btn btn-" + variant()', '[attr.aria-busy]': 'loading()' },
template: `
<button [type]="type()" [disabled]="disabled() || loading()" (click)="clicked.emit($event)">
@if (loading()) { <span class="spinner" aria-hidden="true"></span> }
<ng-content />
</button>`
})
export class UiButton {
variant = input<'primary' | 'secondary' | 'danger'>('primary');
type = input<'button' | 'submit'>('button');
disabled = input(false, { transform: booleanAttribute }); // permite <ui-button disabled>
loading = input(false, { transform: booleanAttribute });
clicked = output<MouseEvent>();
}
<!-- ui-card.html -->
<header><ng-content select="[card-title]" /></header>
<section><ng-content /></section>
<footer><ng-content select="[card-actions]" /></footer>
<!-- uso -->
<ui-card>
<h3 card-title>Pedido 1024</h3>
<p>3 líneas · $1,250</p>
<ui-button card-actions (clicked)="ver()">Ver</ui-button>
</ui-card>
Para tablas reutilizables donde el que usa decide cómo pintar cada celda: ng-template + ngTemplateOutlet con contexto (let-row). Es el patrón de mat-table.
@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) => void = () => {};
private onTouched: () => void = () => {};
writeValue(v: number | null) { this.valor.set(v ?? 0); } // form → componente
registerOnChange(fn: (v: number) => void) { this.onChange = fn; } // componente → form
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();
}
}
// uso: <ui-cantidad formControlName="cantidad" [min]="1" />
writeValue (entra), registerOnChange (sale), registerOnTouched (tocado), setDisabledState (deshabilitar). Y el provider NG_VALUE_ACCESSOR con multi: true./* styles/tokens.scss */
:root {
--color-primary: #1f5fa8; --color-danger: #b3261e;
--space-2: 8px; --space-4: 16px; --radius: 8px;
--font-body: 14px/1.5 system-ui;
}
/* ui-button.scss — solo tokens, nunca hex sueltos */
:host { display: inline-block; }
.btn-primary button { background: var(--color-primary); padding: var(--space-2) var(--space-4); border-radius: var(--radius); }
Emulated (default) aísla estilos; :host estiliza el propio elemento; no uses ::ng-deep (deprecado) — expón variables CSS.mat.theme(...) y overrides) en vez de sobrescribir clases internas.Directiva cuando agregas comportamiento a un elemento existente sin cambiar su plantilla (autofocus, permisos, máscara de input). Componente cuando tienes plantilla propia. Para componer comportamientos: hostDirectives.
appPreventDoubleClick: intercepta el clic, hace preventDefault y stopPropagation, lo pasa por un Subject con throttleTime configurable y emite un output throttledClick. Se desuscribe en ngOnDestroy. Hoy está en 23 botones: pago, envío, cancelación de folios y de pedidos a domicilio. Es comportamiento reutilizable sin tocar la plantilla de nadie."throttleTime bloquea por tiempo fijo, 800 ms. Si el servidor tarda más, un tercer clic pasaría. Hoy lo haría con exhaustMap atado a la petición: ignora clics mientras la operación sigue en curso, dure lo que dure."@Directive({ selector: '[appSiPermiso]' })
export class SiPermisoDirective {
private tpl = inject(TemplateRef); private vcr = inject(ViewContainerRef);
private sesion = inject(SesionService);
permiso = input.required<string>({ alias: 'appSiPermiso' });
constructor() {
effect(() => {
this.vcr.clear();
if (this.sesion.tiene(this.permiso())) this.vcr.createEmbeddedView(this.tpl);
});
}
}
// <button *appSiPermiso="'pedidos.cancelar'">Cancelar</button>
InjectionToken o de una clase abstracta, y cada contexto provee su implementación. Es justo lo que me pidieron en una prueba: un buscador sin HttpClient. El componente recibe un ProductSearchService por token; en producción es HTTP, en pruebas es un arreglo en memoria."ng g library ui → se compila con ng-packagr, expone un public-api.ts (lo que no se exporta ahí es privado), versionada con semver. En el backoffice era common/tools/projects/libreria, consumida por los remotos y compartida en federation para no duplicarla.
Usa el elemento nativo (<button>, no <div (click)>), aria-* cuando no hay nativo, foco visible, navegación con teclado. El CDK trae FocusTrap, LiveAnnouncer y ListKeyManager para no reinventarlo.
¿Cómo organizas el consumo de APIs?
HttpClient. Hay una capa de datos por feature que sabe las URLs y los tipos, y un servicio que orquesta: loading, errores, transformar la respuesta. En el backoffice era un patrón de cuatro capas: interfaz del contrato, un assembly que arma el request, la capa que hace el HTTP y el servicio que orquesta. Lo transversal va en interceptores: en el punto de venta había cuatro — el que agrega el token, uno de timeout, uno de logging y uno que valida el contexto de venta. Y para los errores que no son HTTP hice un ErrorHandler global."ErrorHandler global que captura cualquier excepción no controlada y la manda a un servicio de log con el contexto que necesitábamos para reproducirla: sucursal, caja, IP, versión desplegada, la URL, y el método y la línea sacados del stack. Además trata aparte el ChunkLoadError: pasa cuando publicas una versión nueva y una terminal que tenía la vieja abierta intenta cargar un módulo lazy que ya no existe. Ese lo registramos distinto porque la causa es el despliegue, no el código. Y filtra los errores de red de status 0 para no llenar el log de ruido."ErrorHandler ve todo lo no controlado: excepciones de JavaScript, promesas rechazadas, fallos de carga de chunks. Se complementan: el interceptor maneja y el ErrorHandler registra lo que se escapó.// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withInterceptors([authInterceptor, loadingInterceptor, errorInterceptor])),
]
};
// features/pedidos/data/pedidos.api.ts
@Injectable({ providedIn: 'root' })
export class PedidosApi {
private http = inject(HttpClient);
private base = `${environment.apiUrl}/pedidos`;
listar(f: FiltroPedidos) {
const params = new HttpParams({ fromObject: { page: f.page, size: f.size, estatus: f.estatus ?? '' } });
return this.http.get<PagedResult<Pedido>>(this.base, { params });
}
obtener(id: number) { return this.http.get<Pedido>(`${this.base}/${id}`); }
crear(p: NuevoPedido) { return this.http.post<Pedido>(this.base, p); }
actualizar(id: number, p: NuevoPedido) { return this.http.put<void>(`${this.base}/${id}`, p); }
eliminar(id: number) { return this.http.delete<void>(`${this.base}/${id}`); }
}
// 1. Token
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(SesionService).token();
if (!token || req.url.includes('/auth/')) return next(req);
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })); // req es inmutable
};
// 2. Loading global con opción de excluir
export const SIN_LOADING = new HttpContextToken<boolean>(() => false);
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
if (req.context.get(SIN_LOADING)) return next(req);
const loading = inject(LoadingService);
loading.inc();
return next(req).pipe(finalize(() => loading.dec())); // contador, no booleano
};
// uso: this.http.get(url, { context: new HttpContext().set(SIN_LOADING, true) })
// 3. Errores centralizados
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router); const notif = inject(NotificacionService);
return next(req).pipe(catchError((e: HttpErrorResponse) => {
if (e.status === 401) router.navigate(['/login']);
else if (e.status === 403) notif.error('No tienes permiso');
else if (e.status === 0) notif.error('Sin conexión con el servidor');
else if (e.status >= 500) notif.error('Error del servidor, intenta de nuevo');
return throwError(() => e); // re-lanza: el que llamó decide si hace algo más
}));
};
finalize corre en éxito, error y cancelación.En el interceptor de errores, al recibir 401: llamar a /refresh una sola vez (compartido con shareReplay o un flag para que diez peticiones fallidas no disparen diez refresh), y reintentar la petición original con el token nuevo vía switchMap. Si el refresh falla → logout.
Funcional (HttpInterceptorFn) es el recomendado desde Angular 15: sin clase, con inject(), registrado con withInterceptors. Los de clase siguen funcionando con withInterceptorsFromDi() — útil al migrar código viejo.
CORS lo resuelve el servidor (cabeceras Access-Control-Allow-*), no Angular. En desarrollo: proxy.conf.json en ng serve para que el navegador vea mismo origen. En el backoffice había un proxy .NET para el visor de reportes SSRS justamente por eso.
Lo más seguro: cookie HttpOnly + Secure + SameSite que pone el backend (JavaScript no la lee → XSS no la roba). localStorage es común pero expuesto a XSS. Si es localStorage, compensa con CSP estricta y tokens de vida corta.
provideHttpClient() + provideHttpClientTesting() y HttpTestingController: expectOne(url), req.flush(datos), verify() en el afterEach. Ver pestaña Testing.
¿Cómo optimizas una app Angular?
@defer para lo pesado dentro de una pantalla, y presupuestos de bundle en angular.json. Render: componentes OnPush con estado en signals, y track en las listas para no recrear el DOM. Datos: no pedir lo que no se ve — paginación en servidor, debounce en búsquedas, cancelar peticiones viejas con switchMap. Y mido antes de optimizar, con Angular DevTools y Lighthouse. En el backoffice el lazy loading lo llevamos a nivel de microfrontend: cada módulo solo se descarga cuando el usuario entra."Con ChangeDetectionStrategy.OnPush Angular revisa el componente solo cuando:
async pipe, o cambia un signal que la plantilla lee.markForCheck() a mano.this.pedido.estatus = 'X' en el padre no actualiza un hijo OnPush — la referencia es la misma. Inmutabilidad: this.pedido = { ...this.pedido, estatus: 'X' }, o mejor, estado en signals.<!-- Angular 17+: track es OBLIGATORIO en @for -->
@for (p of pedidos(); track p.id) { <app-pedido-card [pedido]="p" /> }
<!-- Equivalente viejo -->
<app-pedido-card *ngFor="let p of pedidos; trackBy: porId" [pedido]="p" />
porId = (_: number, p: Pedido) => p.id;
track $index: sirve para listas estáticas, pero si reordenas o insertas, reutiliza mal los elementos."{ path: 'reportes', loadComponent: () => import('./features/reportes/reportes.page').then(m => m.ReportesPage) }
@defer (on viewport; prefetch on idle) {
<app-grafica-ventas [datos]="ventas()" /> <!-- su código va en otro chunk -->
} @placeholder (minimum 300ms) {
<div class="skeleton"></div>
} @loading (after 150ms) {
<app-spinner />
} @error { <p>No se pudo cargar la gráfica</p> }
ChunkLoadError. En el POS lo detectaba tu GlobalErrorHandler y lo registraba aparte. Solución típica: al detectarlo, recargar la página una vez, o usar SwUpdate para avisar que hay versión nueva.Triggers de @defer: on idle (default), viewport, interaction, hover, immediate, timer(2s), when condicion. Estrategias de precarga de rutas: withPreloading(PreloadAllModules) o una propia.
| Técnica | Qué resuelve |
|---|---|
| Pipes puros en vez de métodos en plantilla | {{ calcular(x) }} se ejecuta en cada ciclo; un pipe puro o un computed solo cuando cambia la entrada |
NgOptimizedImage (ngSrc) | Lazy de imágenes, srcset, prioridad para el LCP, evita saltos de layout |
| CDK Virtual Scroll | Listas de miles de filas: solo pinta las visibles |
Budgets en angular.json | El build falla si el bundle crece de más |
source-map-explorer / esbuild stats | Ver qué librería infla el bundle |
| Zoneless | Menos ciclos de change detection y bundle más chico |
| SSR + hydration incremental | Primer pintado rápido (si el proyecto lo necesita; un backoffice detrás de login normalmente no) |
@defer, quitar librerías pesadas.Tu última entrevista fue teclear un componente completo. Estos seis cubren los imprescindibles de la vacante. Protocolo para cada uno: (1) repite el enunciado con tus palabras, (2) pregunta restricciones (¿Material? ¿puedo usar HttpClient? ¿versión?), (3) di la estructura antes de teclear, (4) narra mientras escribes.
Enunciado típico: "Un input que busca productos mientras escribes; muestra resultados, estado de carga y error."
export interface Producto { sku: string; nombre: string; precio: number; }
export abstract class BuscadorProductos { // abstracción: no dependo de HttpClient
abstract buscar(q: string): Observable<Producto[]>;
}
@Component({
selector: 'app-buscador',
imports: [ReactiveFormsModule, CurrencyPipe],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<input [formControl]="q" placeholder="Buscar (mín. 3 letras)" aria-label="Buscar producto" />
@if (cargando()) { <p>Buscando…</p> }
@if (error()) { <p role="alert">{{ error() }}</p> }
<ul>
@for (p of resultados(); track p.sku) {
<li (click)="elegido.emit(p)">{{ p.nombre }} — {{ p.precio | currency:'MXN' }}</li>
} @empty { @if (buscado()) { <li>Sin resultados</li> } }
</ul>`
})
export class BuscadorComponent {
private api = inject(BuscadorProductos);
elegido = output<Producto>();
q = new FormControl('', { nonNullable: true });
cargando = signal(false);
error = signal<string | null>(null);
buscado = signal(false);
resultados = toSignal(
this.q.valueChanges.pipe(
map(v => v.trim()),
debounceTime(300),
distinctUntilChanged(),
tap(() => this.error.set(null)),
switchMap(q => {
if (q.length < 3) { this.buscado.set(false); return of([]); }
this.cargando.set(true);
return this.api.buscar(q).pipe(
catchError(() => { this.error.set('No se pudo buscar'); return of([]); }),
finalize(() => { this.cargando.set(false); this.buscado.set(true); })
);
})
), { initialValue: [] as Producto[] }
);
}
// provider: { provide: BuscadorProductos, useClass: BuscadorProductosHttp }
debounceTime para no pegarle al servidor en cada tecla, distinctUntilChanged para no repetir la misma búsqueda, switchMap para cancelar la anterior y evitar la race condition, catchError dentro para que un error no mate el flujo, y toSignal para que la plantilla lea un signal sin suscripciones manuales."Enunciado típico: "Captura de un pedido: proveedor, RFC válido, y N líneas con SKU y cantidad mayor a cero. No se puede guardar sin al menos una línea."
Solución completa en la pestaña Reactive Forms (formulario + plantilla + rfcValidator + rangoFechas). Practícalo sin mirar hasta que salga en menos de 12 minutos.
NonNullableFormBuilder, validador que devuelve null, formArrayName + [formGroupName]="i", markAllAsTouched() al enviar inválido, getRawValue().Enunciado típico: "Haz un selector de cantidad (− n +) que se pueda usar con formControlName."
Solución en la pestaña Componentes reutilizables → UiCantidad con ControlValueAccessor.
formControlName implemento ControlValueAccessor: writeValue recibe el valor del formulario, registerOnChange me da la función para avisarle cuando cambia, registerOnTouched para marcarlo como tocado, y setDisabledState para cuando el formulario lo deshabilita. Me registro con NG_VALUE_ACCESSOR."Solución en la pestaña HTTP e interceptores. Tecléalo de memoria: funcional, req.clone({ setHeaders }), catchError por status, throwError al final, registro en withInterceptors.
Enunciado típico: "Tabla de pedidos paginada en servidor, con filtro por estatus. Si cambia el filtro, vuelve a la página 1."
@Injectable() // provisto en la ruta del feature
export class PedidosStore {
private api = inject(PedidosApi);
readonly estatus = signal<string | null>(null);
readonly pagina = linkedSignal({ source: this.estatus, computation: () => 1 }); // se resetea
readonly tamano = signal(20);
private resp = toSignal(
toObservable(computed(() => ({ page: this.pagina(), size: this.tamano(), estatus: this.estatus() }))).pipe(
tap(() => this.cargando.set(true)),
switchMap(f => this.api.listar(f).pipe(
catchError(() => of({ items: [], total: 0 } as PagedResult<Pedido>)),
finalize(() => this.cargando.set(false))
))
), { initialValue: { items: [], total: 0 } as PagedResult<Pedido> }
);
readonly cargando = signal(false);
readonly pedidos = computed(() => this.resp().items);
readonly totalPaginas = computed(() => Math.max(1, Math.ceil(this.resp().total / this.tamano())));
readonly hayAnterior = computed(() => this.pagina() > 1);
readonly haySiguiente = computed(() => this.pagina() < this.totalPaginas());
filtrar(e: string | null) { this.estatus.set(e); }
siguiente() { if (this.haySiguiente()) this.pagina.update(p => p + 1); }
anterior() { if (this.hayAnterior()) this.pagina.update(p => p - 1); }
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<select (change)="store.filtrar($any($event.target).value || null)">
<option value="">Todos</option><option value="PENDIENTE">Pendiente</option>
</select>
<table><tbody>
@for (p of store.pedidos(); track p.id) {
<tr><td>{{ p.folio }}</td><td>{{ p.estatus }}</td></tr>
}
</tbody></table>
<button (click)="store.anterior()" [disabled]="!store.hayAnterior()">‹</button>
{{ store.pagina() }} / {{ store.totalPaginas() }}
<button (click)="store.siguiente()" [disabled]="!store.haySiguiente()">›</button>`
})
export class ListaPedidosPage { store = inject(PedidosStore); }
linkedSignal: en filtrar() haz this.estatus.set(e); this.pagina.set(1);. Funciona igual y es honesto. Menciona que linkedSignal existe para eso.Enunciado típico: "Una tarjeta de producto que muestra datos y avisa al padre cuando lo agregan al carrito. El padre lleva el total."
@Component({
selector: 'app-producto-card',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CurrencyPipe],
template: `
<h4>{{ producto().nombre }}</h4>
<p>{{ producto().precio | currency:'MXN' }}</p>
<button (click)="agregar.emit(producto())" [disabled]="agotado()">Agregar</button>`
})
export class ProductoCard {
producto = input.required<Producto & { existencia: number }>();
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)" /> }
| Restricción | Respuesta |
|---|---|
| "Sin HttpClient" | Abstracción (abstract class o InjectionToken) + implementación en memoria con of(datos).pipe(delay(300)) |
| "Sin RxJS" | Signals + effect con setTimeout para el debounce y un contador de petición para descartar respuestas viejas |
| "Sin librerías de UI" | HTML nativo semántico + CSS con flex/grid. No te pierdas en estilos: funcionalidad primero |
| "Versión vieja de Angular" | @Input()/@Output(), *ngIf/*ngFor + trackBy, BehaviorSubject en vez de signal, takeUntil(destroy$) |
No está en la lista, pero en consultoras casi siempre preguntan "¿haces pruebas unitarias?". Tu experiencia real: Karma + Jasmine en el backoffice, Jest lo conoces, SonarQube para cobertura.
jasmine.createSpyObj para aislar la capa HTTP, el loading y el manejo de errores. Lo importante no era el porcentaje sino verificar que en el caso de error se registrara el log y no se siguiera el flujo."// Componente con signals
describe('CarritoComponent', () => {
it('calcula el total y habilita cobrar', () => {
const f = TestBed.createComponent(CarritoComponent);
f.componentInstance.agregar({ sku: 'A', precio: 10, cantidad: 3 });
f.detectChanges();
expect(f.componentInstance.total()).toBe(30);
expect(f.nativeElement.querySelector('button').disabled).toBeFalse();
});
});
// Servicio HTTP
beforeEach(() => TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()]
}));
it('lista pedidos con params', () => {
const api = TestBed.inject(PedidosApi), http = TestBed.inject(HttpTestingController);
api.listar({ page: 2, size: 20 }).subscribe(r => expect(r.total).toBe(1));
const req = http.expectOne(r => r.url.endsWith('/pedidos') && r.params.get('page') === '2');
req.flush({ items: [{ id: 1 }], total: 1 });
http.verify();
});
// Buscador con debounce
it('no busca antes de 300 ms', fakeAsync(() => {
comp.q.setValue('para'); tick(299); expect(api.buscar).not.toHaveBeenCalled();
tick(1); expect(api.buscar).toHaveBeenCalledOnceWith('para');
}));
Precisión, que aquí es fácil equivocarse: el proyecto Karma (el runner, fuera de Angular) está deprecado desde 2023, pero Angular lo sigue soportando oficialmente para ng test. En Angular 20, Vitest y Jest venían como opciones experimentales; desde Angular 21, Vitest es el default de los proyectos nuevos. Jest y Vitest son más rápidos (corren sin navegador real, sobre jsdom); Karma corre en un navegador real. La API de pruebas de Angular (TestBed) es la misma en los tres.
Comportamiento visible, no detalles internos: dado un input, qué se pinta; dado un clic, qué output se emite o qué método del servicio se llama. Los servicios se mockean. Para componentes de Material, component harnesses (MatButtonHarness) en vez de selectores CSS frágiles.
No lo usas y no tienes que fingirlo. El objetivo es que si preguntan no te quedes en blanco, y que se note que un backend Java no te asusta porque vienes de .NET, que es casi el mismo mundo.
| .NET (lo que sabes) | Java / Quarkus |
|---|---|
| ASP.NET Core Web API | Quarkus REST (antes RESTEasy Reactive), estándar Jakarta REST |
[ApiController] + [HttpGet("{id}")] | @Path("/pedidos") + @GET @Path("/{id}") |
Inyección por constructor, AddScoped | CDI: @Inject, @ApplicationScoped, @RequestScoped |
| Entity Framework / Dapper | Hibernate ORM con Panache |
appsettings.json | application.properties |
NuGet · dotnet CLI | Maven/Gradle · quarkus CLI (quarkus dev = hot reload) |
record de C# | record de Java (desde 16) |
async/await, Task | Virtual threads (preview en 20, estables en 21) · Mutiny Uni/Multi en Quarkus |
| Native AOT | GraalVM native image — la razón de ser de Quarkus: arranque en milisegundos, poca memoria |
@Path("/pedidos")
@Produces(MediaType.APPLICATION_JSON)
public class PedidoResource {
@Inject PedidoService service;
@GET @Path("/{id}")
public Response obtener(@PathParam("id") long id) {
return service.buscar(id)
.map(p -> Response.ok(p).build())
.orElse(Response.status(404).build());
}
}
public record PedidoDto(long id, String folio, BigDecimal total) {}
El anuncio dice Java 21, pero te dijeron 20. Conviene saber la diferencia, porque de ahí sale una buena pregunta:
| Java 20 | Java 21 | |
|---|---|---|
| Tipo de versión | No LTS. Salió en marzo 2023; su soporte público terminó en septiembre 2023 | LTS (soporte largo). Salió en septiembre 2023 |
| Virtual threads | Preview (hay que activar --enable-preview) | Estables |
Pattern matching en switch y record patterns | Preview | Estables |
Records, var, text blocks, sealed classes | Estables en las dos (vienen de versiones anteriores) | |
Qué haces con esto: nada de corregir a nadie. Primero confirma que sea Java y no Angular 20 (es fácil que se crucen los números en una llamada). Si es Java 20, haz esta pregunta en la entrevista técnica, que suena a alguien que sabe de ciclo de vida de versiones:
--enable-preview, como los virtual threads."A ti, como front, esto te cambia poco: consumes su API REST por contrato (OpenAPI) igual en 20 que en 21.
async/await). En 20 todavía eran preview.switch, sealed classes: el Java moderno se parece bastante a C#.Lo que le importa al front de ese backend: el contrato. Pide el OpenAPI (Quarkus lo expone en /q/openapi y Swagger UI en /q/swagger-ui) y genera los tipos TypeScript desde ahí. Eso es algo concreto que aportas desde el primer día.
| Pregunta | Respuesta |
|---|---|
| ¿Pretensiones salariales? | Da un rango, no un número, y en bruto mensual. Semi Sr Angular en consultora grande CDMX anda en 35–48k brutos (estimación de mercado, no dato de Capgemini). Di: "Estoy buscando entre 40 y 45 mil brutos mensuales, dependiendo del esquema de prestaciones." Si te mudas a CDMX, sube el piso. Luego pregunta: "¿Cuál es el rango que tienen para la posición?" |
| ¿Qué es "PL y PLS"? | Prestaciones de ley (aguinaldo, vacaciones, IMSS) y superiores a la ley (vales de despensa, SGMM = seguro de gastos médicos mayores, FA = fondo de ahorro). Pregunta montos: % del fondo de ahorro, tope de vales, suma asegurada del SGMM |
| ¿Nómina o esquema mixto? ¿Directo con Capgemini? | Pregúntalo tú. La reclutadora es externa: confirma si la contratación es directa con Capgemini o por un tercero, y si es por tiempo indeterminado |
| ¿Modalidad? | La que decidiste en la card de Resumen. Sostenla |
| ¿Escolaridad? | "Pasante de Ingeniería en Tecnologías de la Información y Comunicaciones, Tecnológico de Tlalnepantla. La titulación está en trámite. ¿El cliente pide título o basta carta de pasante?" Nunca "licenciatura concluida" si no tienes el documento: en consultoras lo validan |
| ¿Inglés? | "Técnico: leo documentación y escribo tickets, commits y correos sin problema. La conversación fluida la estoy trabajando." |
| ¿Disponibilidad? | "Inmediata." |
| ¿Por qué dejaste tu último trabajo? | "Terminó la asignación con el cliente. Cerré la migración que lideraba y ahora busco un proyecto de front donde pueda aportar eso mismo." Corto, sin quejas |
S: backoffice de 16+ áreas en una sola app; un cambio en un módulo obligaba a construir y desplegar todo. T: me tocó liderar la migración. A: separé cada área en un remoto con Native Federation, un shell que los carga por manifiesto, y saqué lo común (buscador de productos, loading, manejo de errores) a una librería compartida para no duplicarlo. R: cada módulo se construye y despliega solo; un feature nuevo ya no toca a los demás. Fui el mayor contribuidor del repositorio: unos dos tercios de los commits (815 de 1,251), incluidos tres cuartas partes del shell y dos tercios de la librería común. Los módulos que más trabajé: pedido manual — el primero que se integró —, login, home, recepción electrónica y monitor de pedidos.
S: más de 500 terminales en 86 sucursales; cuando algo fallaba en el navegador de un cajero, soporte solo recibía "no me deja cobrar". T: que cada error llegara con lo necesario para reproducirlo. A: un ErrorHandler global que manda cada excepción no controlada a un servicio de log con sucursal, caja, IP, versión desplegada, URL, y método y línea sacados del stack; ChunkLoadError (módulo lazy que ya no existe tras un deploy) registrado aparte, y los errores de red de status 0 filtrados. R: se diagnostica desde el log sin llamar a la sucursal, y se distingue un bug real de un problema de despliegue.
Nota: los interceptores HTTP del POS (auth, timeout, logging, contexto) los escribieron compañeros; tú los mantuviste. Si preguntan, di "trabajé con ellos", no "los hice".
S: doble clic en "Cobrar", "Siguiente" o "Cancelar pedido" mandaba la operación dos veces. T: resolverlo en todos los botones sin reescribirlos. A: directiva appPreventDoubleClick: Subject + throttleTime configurable, output throttledClick, desuscripción en ngOnDestroy; la metí en los botones compartidos sw-button-payment y sw-button-footer. R: hoy está en 23 botones de pago, envío y cancelación. Lo que aprendí: el tiempo fijo no cubre un servidor lento; hoy usaría exhaustMap atado a la petición.
S: el punto de venta estaba en Angular 17. T: actualizarlo sin detener la operación de las sucursales. A: lo hice por pasos, una versión mayor a la vez: 17→18 en agosto y 18→19.2 en septiembre — este último tocó 204 archivos, con los schematics de ng update y corrigiendo a mano lo que no migraron solos. R: el POS quedó en 19.2 sin interrumpir la venta. Por qué importa para esta vacante: piden v20+, y tú sabes llevar una app de producción de una versión a la siguiente.
ng update va de una en una, porque los schematics de migración están escritos para el salto inmediato."S: la pantalla de inventario/recepción tenía reglas que dependían de varios campos (si se puede agregar un lote). A: estado en signal y reglas en computed (puedeAgregarLote), inject() e input()/output(). R: sin suscripciones que limpiar, la regla vive en un solo lugar y la plantilla solo lee.
S: el buscador se usaba en varios módulos y pegaba al servidor en cada tecla. A: Subject + debounceTime + mínimo de 3 caracteres + takeUntil para liberar al destruir; paginación por hash al hacer scroll en el autocompletado. R: menos peticiones, sin fugas, y resultados grandes sin cargar todo de golpe.
El spinner que bloqueaba la caja (2023). S: en el punto de venta, al buscar un producto o agregarlo al carrito, si el servicio fallaba el spinner de carga se quedaba en pantalla y el cajero no podía seguir: tenía que recargar y perdía la venta en curso. T: encontrar por qué. A: el patrón que usábamos — y que yo también escribía — era mostrar el spinner antes de la petición y ocultarlo solo en el next de la suscripción: pensábamos en el camino feliz. Agregué el manejo en el caso de error en la búsqueda de productos y en el componente del carrito. R y lección: desde entonces el cierre de un estado de carga va donde corre en cualquier caso — hoy, finalize —, y cuando reviso código busco justo eso: ¿qué pasa con la pantalla si esto falla?
finalize, que corre en éxito, error y cancelación."Estructura que cumple: lo reconoces sin culpar a nadie, lo corregiste, y cambiaste tu forma de trabajar.
Graba tu voz o pídele a alguien que te las lea. Cada una en menos de 60 segundos, con el molde. Si titubeas, vuelve a la pestaña y repite.
signal, computed y effect?effect?switchMap, mergeMap, concatMap y exhaustMap, con un caso de cada uno.forkJoin vs combineLatest?ControlValueAccessor y para qué sirve?trackBy / track?@defer?ng new practica --style=scss --ssr=false) por si piden compartir pantalla.app.config.tsloadChildren + providers en ruta)signal estado · computed derivado · effect hacia fueralinkedSignal se resetea con su fuenteinput() · output() · model() · viewChild()update inmutable, nunca pushtoSignal / toObservableswitchMap · en orden → concatMapexhaustMap · paralelo → mergeMapforkJoin todos completan · combineLatest cada cambiocatchError dentro del switchMaptakeUntilDestroyed() al final del pipeNonNullableFormBuilder · tipadosnull = válidoupdateOn: 'blur'formArrayName + [formGroupName]="i"markAllAsTouched() · getRawValue()input/output, sin servicios de negocio<ng-content select> · ngTemplateOutletwriteValue · registerOnChange · registerOnTouched · setDisabledState::ng-deepHttpInterceptorFn + withInterceptors([...])req.clone({ setHeaders }) · loading con contador + finalizemarkForCheck@for (...; track p.id) · loadComponent · @defer (on viewport)Las tres reglas
Sin "creo". Siempre con contexto (86 sucursales, 500 terminales, migración a Angular 20). Término exacto.