<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

25 domande da colloquio per frontend / Angular developer: Da junior a lead (con risposte)

Introduzione

Un colloquio per una posizione Frontend o Angular Developer non è mai una semplice verifica di sintassi. Chi conduce il colloquio vuole capire come ragioni davanti a un problema, quanto a fondo conosci gli strumenti che usi ogni giorno, e come le tue scelte cambiano quando lo scenario passa da "un componente" a "un'applicazione enterprise usata da centinaia di migliaia di utenti".

Questa guida raccoglie 25 domande reali, organizzate in quattro livelli di seniority — Junior, Mid-level, Senior ed Expert/Lead — con risposte complete che puoi usare sia per prepararti a un colloquio sia, se sei dall'altra parte del tavolo, per condurne uno tu stesso in modo strutturato.

Le domande dei primi livelli verificano le fondamenta (HTML, CSS, JavaScript, TypeScript, Angular di base); quelle dei livelli intermedi entrano in RxJS, componenti, servizi, routing e state management; quelle Senior toccano change detection, rendering, performance avanzata, testing e design pattern; quelle Expert/Lead affrontano architetture di grandi applicazioni, microfrontend, SSR, sicurezza e i trade-off tecnici che un lead deve saper argomentare, non solo conoscere.

Argomenti: #Angular #Frontend #JobInterview #TypeScript #JavaScript #RxJS #WebDevelopment #CareerAdvice #SoftwareArchitecture #TechnicalInterview

Come è Strutturata Questa Guida

Ogni domanda è pensata per riflettere ciò che viene realmente chiesto nei colloqui tecnici, non domande "da manuale" isolate dal contesto. Le risposte non si limitano alla definizione formale: spiegano perché la risposta è quella, quando la regola ha eccezioni, e cosa un intervistatore esperto si aspetta di sentire per distinguere una risposta da manuale da una risposta da chi il problema l'ha davvero risolto in produzione.

  • Livello Junior (6 domande): fondamentali HTML, CSS, JavaScript, TypeScript e Angular.
  • Livello Mid-level (6 domande): RxJS, componenti, servizi, routing, state management, performance di base.
  • Livello Senior (7 domande): architettura, change detection, rendering, performance avanzata, testing, design pattern.
  • Livello Expert/Lead (6 domande): architetture di grandi applicazioni, scalabilità, microfrontend, SSR, sicurezza, trade-off tecnici.

Livello Junior — Fondamentali HTML, CSS, JavaScript, TypeScript e Angular

A questo livello, l'obiettivo dell'intervistatore non è trovare l'errore, ma capire se hai basi solide su cui costruire. Le risposte migliori sono quelle che, oltre alla definizione, spiegano l'impatto pratico della conoscenza.

1. Qual è la differenza tra elementi HTML block-level e inline, e perché è importante conoscerla?

Gli elementi block-level (come <div>, <section>, <p>) occupano sempre tutta la larghezza disponibile del genitore e iniziano su una nuova riga, accettando width, height e margini/padding su tutti i lati. Gli elementi inline (come <span>, <a>, <strong>) occupano solo lo spazio necessario al contenuto, si dispongono in linea con il testo circostante e ignorano width/height, oltre a gestire in modo particolare i margini verticali.

Conoscerla è importante perché spiega bug di layout molto comuni: un <span> a cui hai impostato width e non succede nulla, o un elemento che "non si allinea" come ti aspetti. Con display: flex, grid e inline-block questa distinzione classica si sfuma, ma capire il comportamento di default resta fondamentale per prevedere come si comporterà un elemento prima ancora di applicare CSS.

2. Cosa sono il box model CSS e le differenze tra content-box e border-box?

Il box model descrive come il browser calcola le dimensioni finali di un elemento: content (il contenuto), padding (spazio interno), border (bordo) e margin (spazio esterno), concentrici l'uno nell'altro. La proprietà box-sizing determina come width e height vengono interpretati:

/* content-box (default del browser): width/height si riferiscono SOLO al contenuto.
   padding e border si sommano, ingrandendo la dimensione finale renderizzata. */
.box-legacy {
  box-sizing: content-box;
  width: 200px;
  padding: 20px;
  border: 5px solid black;
  /* larghezza finale renderizzata: 200 + 20*2 + 5*2 = 250px */
}

/* border-box: width/height includono padding e border.
   La dimensione dichiarata è la dimensione finale renderizzata. */
.box-modern {
  box-sizing: border-box;
  width: 200px;
  padding: 20px;
  border: 5px solid black;
  /* larghezza finale renderizzata: 200px, esattamente come dichiarato */
}

In pratica, quasi tutti i progetti moderni impostano *, *::before, *::after { box-sizing: border-box; } globalmente proprio perché rende i calcoli di layout prevedibili, evitando che aggiungere un padding "rompa" una griglia già dimensionata.

3. Qual è la differenza tra var, let e const in JavaScript, e quali problemi risolve let?

var ha function scope (o global scope se dichiarata fuori da funzioni) e soffre di hoisting con inizializzazione a undefined, il che permette di "usarla prima di dichiararla" senza errori — un comportamento fonte di bug silenziosi. let e const hanno invece block scope (visibili solo all'interno delle parentesi graffe in cui sono dichiarate) e vivono in una temporal dead zone: accedervi prima della dichiarazione lancia un ReferenceError esplicito invece di restituire silenziosamente undefined.

// Il classico bug da var dentro un loop asincrono
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log('var:', i), 10);
}
// Stampa: var: 3, var: 3, var: 3
// perché var è condivisa da tutte le iterazioni (una sola variabile, function-scoped)

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log('let:', i), 10);
}
// Stampa: let: 0, let: 1, let: 2
// perché let crea un nuovo binding per ogni iterazione del loop

const impedisce la riassegnazione del binding (non rende l'oggetto immutabile: un array o oggetto dichiarato con const può comunque essere mutato al suo interno). La regola pratica che quasi tutti i team seguono: const di default, let solo quando serve riassegnare, var mai in codice nuovo.

4. Cosa sono le closures in JavaScript e a cosa servono in pratica?

Una closure si crea quando una funzione "ricorda" le variabili dell'ambito in cui è stata definita, anche dopo che quell'ambito ha terminato l'esecuzione. In pratica, la funzione interna mantiene un riferimento vivo alle variabili della funzione esterna.

function createCounter() {
  let count = 0; // variabile "chiusa" nella closure

  return {
    increment: () => ++count,
    decrement: () => --count,
    getValue: () => count,
  };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getValue()); // 2

// count non è accessibile dall'esterno: è uno stato privato reso possibile dalla closure
console.log(counter.count); // undefined

Le closures sono ovunque nel codice Angular reale: ogni volta che passi una funzione callback a un subscribe(), a un setTimeout, o definisci un computed()/effect() di un Signal, quella funzione "chiude" sulle variabili del componente. Capire le closures spiega anche un bug frequente: catturare per riferimento una variabile che cambia (es. l'indice di un loop) invece del valore atteso al momento della chiamata.

5. Cosa sono i generics in TypeScript e perché sono utili?

I generics permettono di scrivere funzioni, classi e interfacce che funzionano con tipi diversi senza perdere la sicurezza di tipo, sostituendo l'alternativa peggiore — usare any, che disattiva di fatto il type-checking.

// Senza generics: perdiamo informazione di tipo, il chiamante deve fare un cast manuale
function wrapInArrayUnsafe(value: any): any[] {
  return [value];
}
const result = wrapInArrayUnsafe('hello'); // result è any, nessun autocompletamento

// Con generics: il tipo si propaga automaticamente dall'input all'output
function wrapInArray(value: T): T[] {
  return [value];
}
const strings = wrapInArray('hello'); // TypeScript inferisce string[]
const numbers = wrapInArray(42);      // TypeScript inferisce number[]

// Un caso reale: un servizio HTTP generico
class ApiService {
  constructor(private endpoint: string) {}

  getAll(): Observable {
    return this.http.get(this.endpoint);
  }
}

const productsApi = new ApiService('/api/products');
// productsApi.getAll() restituisce Observable, tipizzato correttamente

In Angular i generics sono ovunque: Observable<T>, signal<T>, Component<T> nei test, i Repository<Entity> lato NestJS. Saperli usare in prima persona (non solo riconoscerli) è ciò che distingue chi scrive codice tipizzato robusto da chi si limita a copiare pattern esistenti.

6. Cos'è un componente Angular e quali sono i suoi elementi principali?

Un componente Angular è una classe TypeScript decorata con @Component che controlla una porzione di interfaccia utente. I suoi elementi principali sono:

  • Decorator @Component: metadati che descrivono selector, template e stile del componente.
  • Template: l'HTML (inline o in file separato) con binding, direttive e interpolazione.
  • Classe: contiene stato (proprietà, signal) e comportamento (metodi, lifecycle hook).
  • Stili: CSS con scope incapsulato al componente di default (View Encapsulation).
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-greeting',
  standalone: true, // non richiede più NgModule dal 2023 in poi
  template: `
    

Ciao, {{ name() }}!

Cambia nome `, styles: [`h2 { color: var(--color-primary); }`], }) export class GreetingComponent { name = signal('Mondo'); changeName(): void { this.name.set('Angular'); } }

Dal 2023 (Angular 14+ per i standalone components, poi default da Angular 17) un componente non ha più bisogno di essere dichiarato in un NgModule: dichiara direttamente le proprie dipendenze tramite imports nel decorator. È il pattern che ci si aspetta venga usato in qualsiasi progetto nuovo oggi.

Livello Mid-level — RxJS, Componenti, Servizi, Routing e State Management

A questo livello l'intervistatore verifica se sai collegare i concetti: non basta sapere cos'è un Observable, bisogna sapere quale operatore RxJS usare in quale scenario, e perché una scelta sbagliata causa bug reali (race condition, memory leak, richieste duplicate).

7. Qual è la differenza tra switchMap, mergeMap, concatMap ed exhaustMap in RxJS, e quando usare ciascuno?

Tutti e quattro sono operatori di "flattening" che gestiscono un Observable di Observable, ma con strategie di concorrenza opposte:

OperatoreComportamentoCaso d'uso tipico
switchMapCancella la richiesta precedente non ancora completata quando ne arriva una nuovaCampo di ricerca con autocomplete (solo l'ultimo input conta)
mergeMapEsegue tutte le richieste in parallelo, senza cancellare nullaUpload multipli di file indipendenti tra loro
concatMapAccoda le richieste, esegue la successiva solo dopo il completamento della precedenteOperazioni che devono avvenire in ordine stretto (es. scrittura sequenziale su un log)
exhaustMapIgnora i nuovi eventi finché quello in corso non è completatoPulsante di submit — evita doppie sottomissioni da click multipli
// L'errore più comune: usare mergeMap per una ricerca con autocomplete
searchInput.valueChanges.pipe(
  debounceTime(300),
  mergeMap(query => this.api.search(query)), // ❌ le risposte possono arrivare fuori ordine!
).subscribe(results => this.results.set(results));

// Se l'utente digita velocemente "an" poi "angular", e la richiesta per "an"
// impiega più tempo a rispondere di quella per "angular", l'utente vede
// i risultati sbagliati (quelli di "an") sovrascrivere quelli corretti.

// La scelta corretta: switchMap cancella la richiesta obsoleta
searchInput.valueChanges.pipe(
  debounceTime(300),
  switchMap(query => this.api.search(query)), // ✅ solo l'ultima richiesta conta
).subscribe(results => this.results.set(results));

Una risposta da Mid-level solida cita esplicitamente questo bug di ordinamento: è la ragione reale per cui switchMap è quasi sempre la scelta corretta per la ricerca, non una regola arbitraria da imparare a memoria.

8. Come funziona la Dependency Injection in Angular e qual è la differenza tra providedIn: 'root' e providers a livello di componente?

Angular mantiene una gerarchia di injector: uno root a livello di applicazione, e uno per ogni componente (e i suoi figli, se non sovrascritto). Quando un componente richiede una dipendenza nel costruttore (o con inject()), Angular cerca un provider risalendo la gerarchia dall'injector del componente fino alla root.

// providedIn: 'root' — un'unica istanza condivisa in tutta l'applicazione (singleton)
@Injectable({ providedIn: 'root' })
export class AuthService {
  private currentUser = signal(null);
}

// providers a livello di componente — una nuova istanza per ogni istanza del componente
@Component({
  selector: 'app-product-form',
  providers: [FormStateService], // ogni  ottiene la SUA istanza
  standalone: true,
})
export class ProductFormComponent {
  private formState = inject(FormStateService);
}

La scelta ha conseguenze reali: se metti uno stato che deve essere condiviso (es. autenticazione) nei providers di un componente invece che in providedIn: 'root', ogni istanza del componente avrà uno stato isolato — un bug classico "perché il mio servizio non vede i dati aggiornati?" che nasce quasi sempre da questa confusione tra scope dell'injector.

9. Cosa sono i Route Guards e quali tipi esistono in Angular moderno?

I guard sono funzioni (in Angular moderno, funzioni pure, non più classi con interfacce) che decidono se una navigazione può procedere, deve essere reindirizzata, o bloccata. I principali sono:

  • CanActivateFn: decide se una rotta può essere attivata (es. controllo autenticazione).
  • CanActivateChildFn: come sopra, ma applicato alle rotte figlie.
  • CanDeactivateFn: decide se si può lasciare una rotta (es. avviso "modifiche non salvate").
  • CanMatchFn: decide se una rotta può essere abbinata affatto, utile per nascondere intere sezioni lazy-loaded a utenti senza permessi.
  • ResolveFn: non è tecnicamente un guard, ma precarica dati prima che la rotta venga attivata.
export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (auth.isAuthenticated()) return true;

  router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
  return false;
};

// registrazione nelle route
export const routes: Routes = [
  { path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] },
];

10. Come gestiresti lo stato condiviso tra componenti non correlati in un'applicazione Angular di medie dimensioni?

Per componenti non correlati (che non hanno relazione padre-figlio diretta), la soluzione standard è un servizio condiviso con scope providedIn: 'root' che espone lo stato tramite Signal (o, in codebase più datate, tramite BehaviorSubject).

@Injectable({ providedIn: 'root' })
export class CartStateService {
  private readonly _items = signal([]);

  readonly items = this._items.asReadonly(); // esposto in sola lettura
  readonly total = computed(() =>
    this._items().reduce((sum, i) => sum + i.price * i.quantity, 0)
  );

  addItem(item: CartItem): void {
    this._items.update(items => [...items, item]);
  }
}

Per applicazioni di medie dimensioni questo pattern basta e avanza: è semplice, type-safe, reattivo, e non richiede dipendenze esterne. Una libreria come NgRx diventa giustificata solo quando emergono esigenze concrete — time-travel debugging, azioni tracciabili con DevTools, logica di stato complessa condivisa tra decine di feature — non "perché è lo standard enterprise". Introdurla prematuramente aggiunge boilerplate senza benefici proporzionati.

11. Cosa sono i Signals in Angular e come si differenziano da un Observable RxJS?

Un Signal è un contenitore reattivo di un valore che notifica automaticamente chi lo legge quando cambia, senza bisogno di subscribe/unsubscribe. Rispetto a un Observable RxJS, la differenza chiave è che un Signal ha sempre un valore corrente sincrono e immediatamente disponibile (si legge chiamandolo come una funzione: count()), mentre un Observable rappresenta uno stream di eventi nel tempo che potrebbe non aver ancora emesso nulla.

import { signal, computed, effect } from '@angular/core';

const count = signal(0);
const doubled = computed(() => count() * 2); // si ricalcola automaticamente

effect(() => {
  console.log('Il valore doppio è ora:', doubled()); // si riesegue ad ogni cambiamento
});

count.set(5); // stampa: Il valore doppio è ora: 10
count.update(v => v + 1); // stampa: Il valore doppio è ora: 12

In pratica: i Signal sono ideali per stato sincrono locale a un componente (contatori, form state, dati derivati), mentre RxJS resta lo strumento giusto per flussi asincroni complessi (debounce su un input, retry su una chiamata HTTP, combinazione di più stream). Angular moderno li fa interoperare con toSignal() e toObservable(), quindi la domanda non è "quale dei due" ma "quale per questo caso specifico".

12. Qual è la differenza tra @Input()/@Output() tradizionali e i nuovi input()/output() basati su signals?

I decoratori @Input()/@Output() tradizionali dichiarano proprietà normali (o EventEmitter) che Angular popola/osserva tramite il suo meccanismo interno; le nuove funzioni input()/output() (Angular 17.1+) restituiscono rispettivamente un Signal in sola lettura e un emettitore tipizzato, integrandosi nativamente con computed() ed effect().

// Stile tradizionale
@Component({ selector: 'app-counter', standalone: true })
export class CounterComponentLegacy {
  @Input() initialValue = 0;
  @Output() valueChange = new EventEmitter();
}

// Stile moderno basato su Signals
@Component({ selector: 'app-counter', standalone: true })
export class CounterComponent {
  initialValue = input(0);                 // Signal, in sola lettura
  initialValueRequired = input.required(); // obbliga il chiamante a passarlo
  valueChange = output();           // tipizzato, senza dover importare EventEmitter

  // Con input() come Signal, puoi derivare stato reattivo direttamente:
  doubledInitial = computed(() => this.initialValue() * 2);
}

Il vantaggio pratico dei nuovi input()/output() non è solo sintattico: essendo Signal, si integrano automaticamente nel grafo di reattività di Angular (utile soprattutto in applicazioni zoneless), e input.required() sposta un errore che prima si scopriva a runtime a un controllo verificabile in fase di compilazione con lo strict template checking.

Livello Senior — Architettura, Change Detection, Rendering, Performance e Testing

A livello Senior, l'intervistatore non cerca la definizione giusta: cerca il ragionamento. Le domande spesso non hanno un'unica risposta corretta, ma valutano se sai argomentare un trade-off con cognizione di causa.

13. Come funziona il meccanismo di Change Detection di Angular e qual è la differenza tra strategia Default e OnPush?

Con la strategia Default, ogni volta che Angular esegue un ciclo di change detection (scatenato da eventi DOM, timer, chiamate HTTP completate, tramite Zone.js), ogni componente dell'albero viene controllato per verificare se i binding del template sono cambiati, indipendentemente da dove sia effettivamente avvenuto l'evento.

Con OnPush, Angular controlla un componente solo quando: (1) un @Input()/signal input cambia per riferimento (non per mutazione interna dell'oggetto), (2) un evento del DOM avviene al suo interno, (3) un Observable a cui è collegato tramite | async emette un nuovo valore, o (4) viene marcato esplicitamente con markForCheck()/aggiornando un Signal che legge.

@Component({
  selector: 'app-product-card',
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  template: `

{{ product().name }} — {{ product().price | currency }}

`, }) export class ProductCardComponent { product = input.required(); } // ❌ Questo NON scatena il change detection su ProductCardComponent con OnPush, // perché muta l'oggetto esistente invece di sostituirlo (stesso riferimento): someProduct.price = 99; // ✅ Questo sì, perché crea un nuovo riferimento: this.products.update(list => list.map(p => p.id === someProduct.id ? { ...p, price: 99 } : p) );

Una risposta Senior deve citare esplicitamente il concetto di uguaglianza per riferimento: è la causa numero uno del bug "ho aggiornato il dato ma la UI non si aggiorna" quando si passa a OnPush senza adottare pattern immutabili in modo coerente in tutta l'applicazione.

14. Cos'è zoneless Angular e come cambia il modello di change detection?

Storicamente Angular usa Zone.js per "monkey-patchare" le API asincrone del browser (eventi, timer, promise, fetch) e sapere automaticamente quando potrebbe essere necessario un ciclo di change detection. Il modello zoneless (stabile da Angular 18+) rimuove del tutto questa dipendenza: Angular si affida esclusivamente ai Signal per sapere esattamente cosa è cambiato, senza dover controllare l'intero albero "per sicurezza" ogni volta che accade qualcosa di asincrono in background.

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';

bootstrapApplication(AppComponent, {
  providers: [provideZonelessChangeDetection()],
});

I vantaggi pratici: bundle più piccolo (Zone.js pesa circa 30KB), change detection significativamente più mirata (solo i componenti che dipendono da un Signal cambiato vengono aggiornati), e debugging più prevedibile perché ogni aggiornamento UI è riconducibile a un cambiamento di Signal esplicito, non a un evento asincrono generico intercettato "a ombrello". Lo svantaggio: librerie di terze parti datate che si aspettano Zone.js (alcune versioni di componenti Material, librerie grafiche) possono richiedere un NgZone.run() manuale per continuare a funzionare correttamente in modalità zoneless.

15. Come diagnosticheresti e risolveresti un problema di performance dovuto a change detection eccessiva in produzione?

Il primo passo non è indovinare: è misurare con Angular DevTools, tab Profiler. Registrando un'interazione problematica (es. digitare in un campo di ricerca), il Profiler mostra un grafico a barre dove ogni barra è un componente controllato in quel ciclo — se un componente estraneo all'interazione appare ripetutamente, è il segnale che manca OnPush o che c'è un pattern reattivo mal progettato.

Il percorso di risoluzione tipico, in ordine di impatto:

  1. Applicare ChangeDetectionStrategy.OnPush ai componenti "foglia" più costosi da renderizzare, verificando che i dati fluiscano in modo immutabile.
  2. Sostituire mutazioni dirette di array/oggetti con pattern immutabili (spread, map/filter) o passare a Signal, che rendono l'uguaglianza per riferimento automatica e corretta.
  3. Aggiungere track corretto (l'id univoco, non l'indice) nei blocchi @for per evitare che Angular distrugga e ricrei nodi DOM inutilmente quando una lista viene riordinata.
  4. Valutare l'adozione di zoneless per eliminare cicli di change detection scatenati da eventi asincroni non correlati alla UI visibile.

Una risposta Senior efficace menziona sempre Angular DevTools Profiler come primo passo: ottimizzare "a sensazione" senza dati di profiling reali è un errore comune anche a livello Senior, e chi conduce il colloquio lo nota immediatamente.

16. Quali design pattern applichi più spesso in un'architettura Angular enterprise?

PatternApplicazione in Angular
FacadeUn servizio che nasconde la complessità di più servizi/store sottostanti dietro un'API semplice per i componenti (es. CartFacade che orchestra CartService, PricingService, InventoryService).
RepositoryUn servizio dedicato all'accesso ai dati (HTTP, cache locale) che isola i componenti dai dettagli implementativi della sorgente dati.
StrategyInjection di implementazioni intercambiabili tramite InjectionToken, utile per comportamenti che variano per ambiente o configurazione (es. strategie di pagamento diverse).
Smart/Dumb ComponentSeparazione tra componenti "container" (gestiscono stato e side-effect) e componenti "presentazionali" (ricevono dati via input, emettono eventi via output, senza dipendenze dirette da servizi).
AdapterIsolare la forma dei dati esterni (API di terze parti) dietro un mapping verso i modelli interni dell'applicazione, così un cambio dell'API esterna impatta un solo punto del codice.

Una risposta Senior distingue chiaramente quando ogni pattern aggiunge valore e quando invece è overengineering: introdurre un Facade per un singolo servizio semplice, ad esempio, aggiunge indirection senza benefici reali.

17. Come strutturi il testing di un'applicazione Angular complessa, e quali trade-off consideri?

Seguo la piramide dei test: molti unit test veloci e isolati (service, pipe, logica pura estratta dai componenti), un numero moderato di test di integrazione (componenti con TestBed, verificando l'interazione reale con i servizi), e pochi test E2E mirati sui flussi realmente critici per il business (login, checkout), non su ogni singola pagina.

// Unit test: logica pura, veloce, nessuna dipendenza da Angular
describe('calculateDiscount', () => {
  it('applica correttamente uno sconto percentuale', () => {
    expect(calculateDiscount(100, 20)).toBe(80);
  });
});

// Integration test: componente reale con TestBed, verifica il comportamento visibile
describe('ProductCardComponent', () => {
  it('emette addToCart quando si clicca il pulsante', () => {
    const fixture = TestBed.createComponent(ProductCardComponent);
    fixture.componentRef.setInput('product', mockProduct);
    fixture.detectChanges();

    let emitted: Product | undefined;
    fixture.componentInstance.addToCart.subscribe(p => (emitted = p));
    fixture.debugElement.query(By.css('[data-testid="add-btn"]')).nativeElement.click();

    expect(emitted).toEqual(mockProduct);
  });
});

Il trade-off principale che argomento sempre in colloquio: gli E2E danno la massima fiducia reale (testano l'app come la userebbe un utente) ma sono lenti e più fragili; gli unit test sono velocissimi ma non catturano problemi di integrazione. Il rapporto giusto non è fisso, dipende dalla criticità del dominio — un flusso di pagamento merita più copertura E2E di una pagina informativa statica.

18. Cos'è il tree-shaking e come le scelte architetturali lo influenzano?

Il tree-shaking è il processo con cui il bundler (esbuild in Angular 17+/20+) elimina dal bundle finale il codice esportato ma mai effettivamente importato o utilizzato, riducendo la dimensione del JavaScript scaricato dal browser.

Le scelte architetturali lo influenzano in modo concreto: i standalone components con imports espliciti rendono le dipendenze statiche e analizzabili, migliorando il tree-shaking rispetto ai vecchi NgModule con declarations "a ombrello" che spesso importavano più del necessario. Anche le barrel file (index.ts che ri-esportano tutto un modulo) possono peggiorare il tree-shaking se il bundler non riesce a determinare staticamente quali export siano effettivamente usati, portando a includere codice morto nel bundle finale.

// ❌ Import "largo": può impedire il tree-shaking se lodash non è in formato ESM puro
import _ from 'lodash';
const chunks = _.chunk(array, 3);

// ✅ Import mirato: il bundler include SOLO la funzione realmente usata
import chunk from 'lodash/chunk';
const chunks = chunk(array, 3);

19. Come implementeresti caching lato client per ridurre chiamate HTTP ridondanti in modo scalabile?

La soluzione più elegante in Angular moderno è un HttpInterceptor che intercetta le richieste GET e restituisce una risposta cachata quando disponibile e non scaduta, invalidando la cache in modo mirato quando i dati sottostanti cambiano.

@Injectable()
export class CacheInterceptor implements HttpInterceptor {
  private cache = new Map; expiry: number }>();

  intercept(req: HttpRequest, next: HttpHandler): Observable> {
    if (req.method !== 'GET') return next.handle(req);

    const cached = this.cache.get(req.urlWithParams);
    if (cached && cached.expiry > Date.now()) {
      return of(cached.response.clone());
    }

    return next.handle(req).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          this.cache.set(req.urlWithParams, {
            response: event,
            expiry: Date.now() + 60_000, // 60 secondi
          });
        }
      }),
    );
  }
}

Il punto che distingue una risposta Senior: sapere cosa cachare e cosa no. Dati di catalogo cambiano raramente e si prestano bene al caching; dati specifici dell'utente autenticato richiedono una chiave di cache che includa l'identità dell'utente (o vanno esclusi del tutto), altrimenti si rischia di mostrare dati di un utente a un altro — un errore di sicurezza, non solo di performance.

Livello Expert / Lead — Architetture Enterprise, Scalabilità, Microfrontend, SSR e Sicurezza

Le domande Expert/Lead raramente hanno una risposta "giusta" univoca. Valutano la capacità di argomentare un trade-off, difendere una decisione con dati concreti, e riconoscere quando la soluzione "più sofisticata" è in realtà quella sbagliata per il contesto.

20. Quando ha senso adottare un'architettura a microfrontend rispetto a un monolite modulare Angular, e quali sono i trade-off reali?

I microfrontend hanno senso quando esiste un problema organizzativo reale, non solo tecnico: più team che devono deployare in modo indipendente, stack o versioni Angular diverse tra le parti dell'applicazione, o la necessità di isolare completamente il ciclo di rilascio di una sezione critica dal resto.

AspettoMonolite modulareMicrofrontend
Complessità operativaBassa — una sola build, un solo deployAlta — orchestrazione, versioning, contratti tra team
Deploy indipendentiNoSì, per team/sezione
Duplicazione di dipendenze runtimeNessunaRischio concreto (più copie di Angular/RxJS) se non gestita con module federation
Coerenza UX tra sezioniNaturaleRichiede governance esplicita (design system condiviso)
Onboarding di nuovi sviluppatoriPiù semplice, un solo repo/architetturaPiù complesso, serve capire i confini tra applicazioni

La mia posizione in un colloquio: un monolite modulare ben strutturato con confini di feature chiari (lazy-loaded, con interfacce esplicite tra i moduli) risolve la maggior parte dei problemi che i team pensano di dover risolvere con i microfrontend, con una frazione della complessità operativa. I microfrontend sono uno strumento organizzativo per problemi di scala di team, non una "architettura migliore" in astratto — e vanno adottati solo quando il costo del coordinamento tra team supera già oggi il costo della complessità tecnica aggiuntiva.

21. Come progetteresti la strategia di SSR/hydration per un'applicazione Angular enterprise con requisiti SEO stringenti?

Partirei da Angular Universal con provideClientHydration() per l'hydration completa, valutando hydration incrementale (withIncrementalHydration()) per le sezioni pesanti della pagina non necessarie al primo render (grafici, editor rich-text, mappe), combinata con @defer per posticiparne il caricamento fino a quando entrano in viewport.

// app.config.server.ts
import { provideServerRendering } from '@angular/platform-server';
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';

export const serverConfig = [
  provideServerRendering(),
  provideClientHydration(withIncrementalHydration()),
];
<!-- Il componente si idrata solo quando entra in viewport, non al bootstrap iniziale -->
@defer (on viewport) {
  <app-heavy-dashboard-chart [data]="chartData()" />
} @placeholder {
  <div class="chart-skeleton"></div>
}

Per i requisiti SEO stringenti, la parte architetturale critica non è solo il rendering: serve un SeoService centralizzato che aggiorni dinamicamente title, meta description, canonical e JSON-LD durante il rendering server-side (non dopo, quando è troppo tardi per i crawler che non eseguono JavaScript), più una strategia esplicita per gestire codice che accede ad API browser-only (window, document) dietro un controllo isPlatformBrowser(), per evitare crash durante il rendering lato server.

22. Quali misure di sicurezza sono essenziali in un'applicazione Angular enterprise?

  • XSS (Cross-Site Scripting): Angular sanitizza automaticamente i binding [innerHTML]/interpolazione, ma bypassSecurityTrustHtml() va usato solo su contenuto realmente fidato — mai su input utente non validato.
  • CSRF (Cross-Site Request Forgery): HttpClientXsrfModule gestisce automaticamente il pattern double-submit-cookie, ma solo se il backend imposta correttamente il cookie XSRF-TOKEN.
  • Content Security Policy (CSP): header configurato lato server che limita le fonti da cui possono essere caricati script/stili, mitigando l'impatto anche di un XSS riuscito.
  • Gestione dei token JWT: access token di vita breve, refresh token gestito lato server (mai leggibile da JavaScript se possibile, tramite cookie httpOnly), e invalidazione reale al logout.
  • Validazione lato client come UX, non come sicurezza: ogni validazione client-side va sempre replicata lato server — un client può essere manipolato con gli strumenti di sviluppo.

Una risposta Expert/Lead deve sottolineare esplicitamente che la sicurezza client-side è una difesa in profondità, non l'unica linea di difesa: chi pensa che validare/ proteggere solo lato Angular sia sufficiente ha una lacuna concettuale seria per questo livello.

23. Come gestiresti il versionamento e la migrazione incrementale di una codebase Angular legacy verso standalone/signals su un team di 20+ sviluppatori?

Non con un "big bang rewrite" — troppo rischioso su un team di quella dimensione. La strategia che adotto è la migrazione incrementale per confini di feature: Angular supporta la coesistenza di NgModule e componenti standalone nella stessa applicazione, quindi si può migrare un modulo alla volta, verificando che ogni step sia deployabile in produzione indipendentemente.

  1. Automatizzare il primo passo con lo schematic ufficiale ng generate @angular/core:standalone, che converte automaticamente componenti/direttive/pipe.
  2. Stabilire un confine chiaro: nuove feature si scrivono solo in standalone da subito, per evitare che il debito tecnico continui a crescere durante la migrazione.
  3. Migrare i moduli legacy in ordine di "raggio di impatto" crescente — prima le feature isolate e a basso traffico, poi progressivamente il core dell'applicazione.
  4. Introdurre i Signal in parallelo ma separatamente dalla migrazione a standalone: sono due assi ortogonali, non serve farli insieme e mescolarli aumenta il rischio per singolo cambiamento.

Il punto che un intervistatore Lead cerca: la capacità di sequenziare il rischio, non solo di conoscere gli strumenti di migrazione. Comunicare chiaramente al team perché si migra gradualmente (e non tutto insieme) è parte del lavoro tanto quanto scrivere il codice.

24. Quali criteri usi per decidere tra NgRx, un semplice Signal Store custom, o nessuna libreria di state management?

CriterioNessuna libreria (Signal/servizi)Signal Store custom leggeroNgRx
Complessità del dominioBassa/mediaMediaAlta, con molte azioni interconnesse
Necessità di time-travel debuggingNoNo
Team abituato a pattern ReduxNon richiestoNon richiestoVantaggio se già presente
Boilerplate accettabileMinimoModeratoSignificativo, mitigato dagli schematics ufficiali
Testabilità dello stato isolatoBuonaOttimaOttima, con pattern consolidati

Il mio criterio decisionale in un colloquio: parto sempre dall'opzione più semplice (Signal in un servizio providedIn: 'root') e la evolvo solo quando emergono sintomi concreti — logica di stato duplicata tra feature, necessità reale di debugging avanzato, o un team che già lavora bene con pattern Redux su altri progetti. Introdurre NgRx "di default" su ogni progetto nuovo è una scelta che tende a rallentare lo sviluppo iniziale senza benefici proporzionati fino a quando la complessità reale non lo giustifica.

25. Come bilanceresti velocità di sviluppo, performance e manutenibilità in una decisione architetturale con deadline stringenti?

Un esempio concreto di trade-off reale: un team deve consegnare una dashboard con grafici interattivi in due settimane. La libreria di grafici più performante richiede tre giorni di integrazione e configurazione avanzata; una libreria più semplice, meno ottimizzata ma con documentazione eccellente, richiede mezza giornata.

La decisione che argomenterei: usare la libreria semplice ora, ma isolarla dietro un'interfaccia/adapter interno esplicito (non chiamarla direttamente da ogni componente), così che se in futuro le metriche di performance reali (non ipotetiche) mostrano che serve la libreria più sofisticata, la sostituzione tocca un solo punto del codice invece di essere una riscrittura diffusa.

Il principio generale che comunico sempre in colloquio: velocità di consegna e qualità architetturale non sono sempre in conflitto — spesso il conflitto reale è tra velocità di consegna e scelte irreversibili premature. Investire tempo per mantenere reversibile una decisione (tramite un layer di astrazione minimo, non uno eccessivo) permette di rispettare la deadline senza ipotecare la manutenibilità futura. Le decisioni davvero costose da cambiare in seguito (schema del database, contratti di API pubblici, scelta del framework) meritano più tempo di analisi; le decisioni facilmente reversibili (quale libreria di grafici) non lo meritano, e insistere per "farle bene subito" è spesso tempo speso male rispetto alla deadline.

Come Prepararsi al Meglio per un Colloquio Angular

  • Non limitarti a memorizzare definizioni: per ogni concetto, chiediti "che bug reale previene conoscere questo?" — è esattamente il tipo di risposta che distingue un candidato solido.
  • Fai pratica con codice reale, non solo teoria: costruisci un piccolo progetto che usi Signal, OnPush, lazy loading e almeno un test — la conoscenza pratica emerge sempre nelle domande di follow-up.
  • Prepara esempi concreti dal tuo lavoro: per le domande Senior/Expert, avere un esempio reale (anche semplificato) di un trade-off che hai affrontato vale più di una risposta teorica impeccabile.
  • Studia gli errori comuni, non solo le best practice: sapere cosa va storto quando si applica male un pattern (es. OnPush con mutazioni dirette) dimostra comprensione più profonda che citare solo la regola.
  • Rimani aggiornato sulle versioni recenti: Signal, standalone components, zoneless e il nuovo control flow (@if/@for) sono ormai lo standard atteso in un colloquio Angular nel 2026, non conoscenza opzionale.

Conclusione

Queste 25 domande coprono il percorso naturale di crescita di un Frontend/Angular Developer: dalle fondamenta di HTML/CSS/JavaScript/TypeScript, attraverso RxJS e la gestione dello stato, fino alle decisioni architetturali che un Lead deve saper motivare davanti a un team o a uno stakeholder. Che tu stia preparando un colloquio o conducendone uno, il filo conduttore è sempre lo stesso: capire perché una scelta tecnica è quella giusta in un contesto specifico, non ripetere una regola a memoria.

Se ti prepari per un colloquio Senior o Expert, il consiglio più concreto è questo: scegli tre o quattro decisioni architetturali reali che hai preso (anche imperfette, anche in un progetto personale) e preparati a raccontarle in dettaglio — cosa hai scelto, quali alternative hai scartato e perché, cosa faresti diversamente oggi. È il tipo di risposta che nessuna definizione da manuale può sostituire.

💬 Note dei lettori

0 note

Scrivi una nota

Condividi la tua opinione, un suggerimento o un complimento

Ultime note

Nessuna nota ancora. Sii il primo a commentare!