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".Así se evalúa en una entrevista: primero que entiendes el concepto y luego que lo has usado de verdad. Cada respuesta son dos partes que dices seguidas, en 20–30 segundos. Si quieren más, preguntan; para eso está "Si repreguntan":
Todos los casos salen del historial de git de tus dos proyectos. Donde dice "hice / creé", el archivo lo creaste tú; donde dice "en el módulo X usamos" o "trabajé en", es código del equipo en el que participaste. Cuando no hay caso real, la etiqueta es "Dónde lo aplicaría": dilo así, sin inventar.
¿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? — Misma idea, otra implementación: Core son providers en app.config.ts; Shared son componentes standalone que se importan donde se usan; cada feature es un archivo de rutas con sus propios providers. El backoffice ya es standalone; el punto de venta sigue con NgModules, que siguen soportados.
¿Cómo evitas que un feature importe de otro? — Regla de lint de fronteras y alias como @shared. Si dos features necesitan lo mismo, se sube a Shared: así nació la librería común del backoffice.
¿Dónde pones el estado? — En el componente si solo él lo usa; en un servicio provisto en la ruta del feature si lo comparte el feature; en root si es global, como la sesión.
¿Por qué microfrontends? — Eran 16 áreas con ritmos de entrega distintos; así cada una se construye y se despliega sola. El costo es compartir dependencias sin duplicar Angular. Para un equipo chico, un monolito modular es mejor.
¿Qué es un signal y por qué lo usas?
signal guarda estado y computed lo deriva.computed: sin suscripciones que limpiar.¿Cuándo usas effect? — Solo para efectos hacia afuera de Angular: storage, logs, una librería externa. Si es calcular un valor a partir de otro, uso computed.
¿Cómo actualizas un arreglo? — Inmutable: update(xs => [...xs, nuevo]). Si haces push, la referencia no cambia y no notifica.
¿Signals reemplaza a RxJS? — No. Signals para estado; RxJS para eventos en el tiempo. Se conectan con toSignal y toObservable.
¿linkedSignal? — Un signal escribible que se reinicia cuando cambia su fuente, como la página que vuelve a 1 al cambiar el filtro.
¿Zoneless? — provideZonelessChangeDetection(), estable desde Angular 20.2: la detección de cambios la disparan signals y eventos, sin Zone.js.
¿Cómo comunicas componentes padre e hijo?
input() y el hijo avisa con output().input() y emite el producto elegido por output(). No sabe qué módulo lo usa.¿Observable o Promesa?
¿switchMap, mergeMap, concatMap o exhaustMap?
switchMap cancela el anterior, concatMap hace cola, mergeMap va en paralelo y exhaustMap ignora hasta terminar.throttleTime, hoy en 23 botones; ahora usaría exhaustMap. En mi buscador cambiaría la promesa por switchMap para cancelar búsquedas viejas.¿Cómo haces varias peticiones a la vez?
forkJoin: las lanza en paralelo y emite una vez, cuando todas terminan.¿Cómo evitas fugas de memoria?
async pipe, toSignal o takeUntilDestroyed().takeUntil(destroy$) y lo emito en ngOnDestroy. Hoy lo cambiaría por takeUntilDestroyed(), que es lo mismo sin el Subject.¿Dónde va el catchError? — Dentro del switchMap: un error completa el observable; adentro solo muere esa petición.
¿Subject o BehaviorSubject? — Subject no guarda valor (eventos); BehaviorSubject guarda el último (estado). En el punto de venta hay servicios con BehaviorSubject para compartir estado; en código nuevo uso signals.
¿Cold vs hot? — Cold: cada suscriptor ejecuta la fuente, como HTTP. Hot: se comparte, como un evento. Para compartir una petición, shareReplay.
¿Por qué Reactive Forms?
¿Cómo haces un validador personalizado?
null si es válido o un objeto con el error.shouldBeLess: el fondo inicial no puede pasar del máximo de la sucursal. Se aplica al grupo y la plantilla muestra el aviso.¿Cómo cambias las validaciones según lo que elige el usuario?
setValidators o clearValidators y luego updateValueAndValidity().¿Cómo manejas una lista dinámica de campos?
FormArray: agregas con push y quitas con removeAt.FormArray de renglones, y el alta de contactos también.¿value o getRawValue? — value excluye deshabilitados; getRawValue() trae todo.
¿setValue o patchValue? — setValue exige todos los campos; patchValue acepta una parte. Para cargar datos a editar, patchValue, que usamos mucho en los dos proyectos.
¿Errores al enviar? — Si es inválido, markAllAsTouched() y los mensajes aparecen porque dependen de touched.
¿Qué hace reutilizable a un componente?
input y output, sin lógica de negocio adentro, y estilos de tokens compartidos.¿Qué es ControlValueAccessor?
formControlName como un input normal.output. Con ControlValueAccessor se usaría directo en el formulario, con validaciones incluidas. Es la mejora que le haría.¿Cómo mantienes la consistencia visual?
¿Qué es un interceptor y para qué lo usas?
ErrorHandler global que registra cada error con sucursal, caja y línea.¿Interceptor o ErrorHandler? — El interceptor ve errores HTTP y reacciona por petición: redirigir en 401, reintentar. El ErrorHandler ve todo lo no controlado — errores de JavaScript, módulos lazy que no cargaron después de un deploy — y lo registra.
¿Cómo agregas el token? — La petición es inmutable: next(req.clone({ setHeaders: { Authorization: 'Bearer ' + token } })).
¿Loading global? — Con contador, no booleano, y se baja en finalize, que corre en éxito, error y cancelación.
¿Refresh token? — En el 401, un solo refresh compartido y reintento de la petición original con switchMap; si falla, cerrar sesión.
¿Cómo funciona OnPush?
¿Para qué sirve trackBy o track?
trackBy por folio, sin reconstruirse enteras.¿Lazy loading o @defer?
@defer carga partes de una misma pantalla.@defer (on viewport) con un placeholder.Una pantalla va lenta, ¿qué haces?
¿Haces pruebas unitarias?
Tu proyecto propio: "Farmacia API" — ya está en Desktop\farmacia-api, funcionando y con 6/6 pruebas. Antes de usar estas respuestas, hazlo tuyo (30 min): córrelo con correr.bat, prueba los endpoints en Swagger, lee los archivos en el orden del LEEME.md y haz el cambio que ahí se indica. Los ejercicios J1–J6 explican cada parte paso a paso.
La frase de entrada, siempre la misma: "En producción no he usado Java; mi backend en el trabajo fue .NET. Pero como la vacante lo pedía, hice un proyecto propio con Quarkus para conocerlo."
Si la entrevista llega antes de que termines el proyecto, usa esta respuesta, que es verdad hoy:
Con esa respuesta, los bloques "Concepto" de esta sección te siguen sirviendo si repreguntan. Los "Cómo lo apliqué · proyecto propio" úsalos solo cuando hayas hecho J1–J6.
¿Has trabajado con Java o Quarkus?
¿Cómo haces un endpoint REST en Quarkus?
@Path en la clase y @GET o @POST en los métodos./productos con búsqueda, obtener por id con 404 si no existe, y crear con 201. Es igual a un controller de .NET.@Path("/productos")
public class ProductoResource {
@Inject InventarioService inventario;
@GET
public List<ProductoDto> buscar(@QueryParam("q") @DefaultValue("") String q) {
return inventario.buscar(q);
}
@GET @Path("/{id}")
public ProductoDto obtener(@PathParam("id") Long id) {
return inventario.obtener(id); // lanza NotFoundException → 404
}
@POST
public Response crear(@Valid ProductoInput input) {
ProductoDto p = inventario.crear(input);
return Response.created(URI.create("/productos/" + p.id())).entity(p).build();
}
}
¿Cómo funciona la inyección de dependencias en Quarkus?
@ApplicationScoped y se pide con @Inject.InventarioService inyectado, igual que separo en Angular.¿Cómo accedes a la base de datos?
persist, findById y list.Producto usa Panache con H2, y descontar existencia es @Transactional: si falla la regla, no se guarda nada.@Entity
public class Producto extends PanacheEntity {
public String sku;
public String nombre;
public BigDecimal precio;
public int existencia;
}
¿Cómo validas lo que llega a la API?
@NotBlank o @Positive y @Valid; si falla, responde 400 solo.public record ProductoInput(
@NotBlank String sku,
@NotBlank String nombre,
@Positive BigDecimal precio,
@PositiveOrZero int existencia) {}
¿Cómo manejas los errores en la API?
StockInsuficienteException y responde 409 con un mensaje que Angular muestra.@ServerExceptionMapper
public RestResponse<Map<String, String>> stock(StockInsuficienteException e) {
return RestResponse.status(Response.Status.CONFLICT, Map.of("error", e.getMessage()));
}
¿Qué usas de Java moderno?
Optional.record y convierto listas con stream().map(...).toList(). Viniendo de C#, es como LINQ.public record ProductoDto(Long id, String sku, String nombre, BigDecimal precio, boolean agotado) {
static ProductoDto from(Producto p) {
return new ProductoDto(p.id, p.sku, p.nombre, p.precio, p.existencia == 0);
}
}
¿Cómo pruebas una API en Quarkus?
@QuarkusTest y RestAssured: haces la petición y revisas el status.@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 verifiqué con una prueba; los tipos TypeScript salen del contrato en Swagger.# application.properties
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:farmacia
quarkus.hibernate-orm.database.generation=drop-and-create
%dev.quarkus.http.cors.enabled=true
%dev.quarkus.http.cors.origins=http://localhost:4200
¿Qué diferencia hay entre Java 20 y 21?
Optional.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, ng generate, ng build, ng test y ng update.ng update, una versión 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? — ci instala exactamente lo del package-lock.json y borra node_modules antes: es reproducible, por eso va en pipelines.
¿Cómo manejas ambientes? — Configuraciones en angular.json con fileReplacements de environment.ts, y ng build --configuration qa.
¿Qué haces si ng update marca problemas? — Una versión mayor a la vez, reviso la guía oficial (update.angular.dev), dejo que corran las migraciones y compilo y pruebo antes de subir.
¿Cómo es tu flujo de trabajo con Git?
DEV; antes de cerrar el PR traía DEV a mi rama y resolvía conflictos ahí.git switch DEV && git pull # base al día
git switch -c DEV_18252_BuscarFacturas # rama de la tarea
git add . && git commit -m "feat: buscar facturas por folio"
git fetch origin && git merge origin/DEV # traer lo nuevo de DEV y resolver conflictos
git push -u origin DEV_18252_BuscarFacturas # y se abre el Pull Request
| 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 une las historias y conserva todo, con un commit de merge; es seguro en ramas compartidas, y es lo que usábamos para traer DEV. rebase reescribe tus commits encima de la base y deja la historia lineal; solo en una rama local que nadie más bajó.
¿Cómo resuelves un conflicto? — Traigo la base a mi rama, abro los archivos marcados, decido línea por línea — con quien hizo el otro cambio si hace falta —, compilo y corro las pruebas antes de hacer commit del merge.
¿revert o reset? — revert para algo que ya está en el remoto, porque no reescribe historia. reset solo para commits locales que nadie más tiene.
¿Qué revisas en un Pull Request? — Que haga lo que pide la tarea, que no rompa otra cosa, nombres claros, que se manejen el error y el estado de carga, y que pasen las pruebas y Sonar.
¿Convención de commits? — Prefijos como feat:, fix:, refactor:, chore: (Conventional Commits) y el número del work item para ligarlo en Azure DevOps.
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.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).
items.update(xs => [...xs, nuevo]) — arreglo nuevo, nunca push.
Un signal escribible que se reinicia cuando cambia su fuente (página que vuelve a 1 al cambiar el filtro).
toSignal(obs$, { initialValue }) y toObservable(signal).
Input como signal (input.required obligatorio); evento hacia el padre; binding de doble vía.
provideZonelessChangeDetection(), estable desde Angular 20.2.
switchMap: cancela la búsqueda anterior. Antes: debounceTime y distinctUntilChanged.
exhaustMap: ignora clics mientras la petición sigue en curso.
concatMap: los pone en cola.
forkJoin emite una vez cuando todos terminan; combineLatest emite cada vez que cualquiera cambia.
async pipe / toSignal; takeUntilDestroyed() al final del pipe; takeUntil(destroy$) en código anterior.
Dentro del switchMap: un error completa el observable; adentro solo muere esa petición.
Subject no guarda valor (eventos); BehaviorSubject guarda el último y necesita valor inicial (estado).
null. Si no, un objeto con el error: { rfc: true }.
En el FormGroup: fb.group({...}, { validators: rangoFechas('desde','hasta') }).
value excluye deshabilitados; getRawValue trae todo.
setValue exige todos los campos; patchValue acepta parcial.
formArrayName="lineas" y dentro [formGroupName]="i" por cada control.
updateOn: 'blur' en el control.
writeValue, registerOnChange, registerOnTouched, setDisabledState + provider NG_VALUE_ACCESSOR con multi: true.
<ng-content select="[card-title]"> por zona y un ng-content sin select para el resto.
Tokens en variables CSS usados por todos los componentes; tema de Material 3 por tokens; catálogo tipo Storybook.
Directiva agrega comportamiento a un elemento existente; componente tiene plantilla propia.
provideHttpClient(withInterceptors([auth, loading, error])) en app.config.ts.
next(req.clone({ setHeaders: { Authorization: 'Bearer ' + t } })) — la petición es inmutable.
Con booleano, la primera petición que termina apaga el spinner de las demás. Se baja en finalize.
Interceptor: errores HTTP, reacciona por petición. ErrorHandler: cualquier error no controlado, para registrarlo.
Nueva referencia en un input, evento en su plantilla, async pipe, signal que lee, o markForCheck.
Identificar cada elemento para no recrear el DOM al llegar una lista nueva. Obligatorio en @for.
Lazy separa por ruta (loadChildren/loadComponent); @defer separa partes de una pantalla (on viewport, interaction…).
Se ejecutan en cada ciclo; usa pipe puro o computed.
Core: una sola vez (interceptores, guards, sesión, layout). Shared: UI reutilizable sin negocio. Feature: área de negocio lazy.
Feature usa Shared y Core; nunca importa de otro feature. Lo compartido se sube a Shared.
En providers de la ruta del feature: vive mientras estás en él.
No. La LTS es 21. En 20, virtual threads y pattern matching en switch eran preview.
@Path("/productos") en la clase, @GET @Path("/{id}") en el método y @PathParam("id") en el parámetro.
El servicio con @ApplicationScoped; en el resource, @Inject InventarioService inventario; (CDI, resuelto al compilar).
id, persist(), findById(), findByIdOptional(), listAll(), list() y find(). Escrituras en @Transactional.
Anotaciones de Bean Validation (@NotBlank, @Positive) en el record y @Valid en el parámetro. Responde 400 automáticamente.
Un método con @ServerExceptionMapper que devuelve RestResponse.status(CONFLICT, ...).
/q/openapi y /q/swagger-ui. Pruebas con @QuarkusTest + RestAssured: given().when().get(...).then().statusCode(200).
lista.stream().map(ProductoDto::from).toList() — como un Select de LINQ.
provideHttpClient() + provideHttpClientTesting(), HttpTestingController.expectOne, flush, verify.
ng g c ruta/nombre · ng g s ruta/nombre · ng g interceptor ruta/nombre.
ng build --configuration production · ng update @angular/core @angular/cli, una versión mayor a la vez.
ci instala exacto desde el lockfile y es reproducible: para pipelines.
Merge une historias y es seguro en ramas compartidas; rebase reescribe y deja historia lineal: solo en tu rama local.
revert para lo que ya está en el remoto (crea un commit nuevo); reset solo para commits locales.
git stash y después git stash pop.
git cherry-pick <hash>.
practica abierto por si piden compartir pantalla.