AngularAtlased. 22.2

Ch. 01Start04 / 75

Angular interview questions

60 real interview questions for junior, mid and senior, with the answer to give, the follow-up and the red flags. Plus a mock interview.

v21v22 ~0 min read

60 questions that really come up in Angular interviews in 2026, split by level: what a junior must know, what is expected from a mid developer who builds features alone, and what a senior is asked about internals, architecture and trade-offs. Each answer starts with what to say first, then what to add to show you understand, the question that usually follows and the answer that fails the interview.

How to use this page

  1. Answer out loud before you open the answer. Reading feels like knowing; saying it is what you will do in the interview.
  2. Start with the short answer, then add one or two ideas from “go deeper”. Interviewers prefer a clear position over a long list.
  3. Mark what you know with “I know this”. It is saved in this browser, and the mock interview gives you the rest first.
  4. Every answer links to the lesson that explains the topic properly.

Mock interview

A few random questions, one at a time, with the answer hidden until you ask for it. At the end you get the list of questions worth reviewing.

LiveInterview simulator

Answer out loud, as if someone were listening. Then reveal the model answer and be honest with yourself.

Level
Questions
Questions you haven’t marked as known come first.

Junior: the fundamentals

For a first Angular job you are not expected to know internals, but you are expected to know the modern basics without hesitating: standalone components, the new control flow, signals, inputs and outputs, services with inject(), routing and forms. Show that you know why, not only how.

0 of 20 marked as known
  1. J-01What is a component, and what does “standalone” mean? How does an app start without an NgModule? Components

    Say this first

    A component is a TypeScript class with @Component that pairs a template with state and behaviour, and is used through its selector. Standalone (the default) means it declares its own dependencies in imports, so no NgModule is needed: the app starts with bootstrapApplication(App, appConfig).

    Then go deeper

    • You no longer write standalone: true — it is the default. Each component imports exactly what its template uses (other components, pipes, RouterLink), so dependencies are explicit and tree-shakable.
    • Application-wide setup lives in app.config.ts as providers: provideRouter(routes), provideHttpClient(...) only if you configure it, and so on.
    • An old library that only exports an NgModule can still go directly into a component’s imports; module providers go through importProvidersFrom().
    import { bootstrapApplication } from '@angular/platform-browser';
    import { App } from './app/app';
    import { appConfig } from './app/app.config';
    
    bootstrapApplication(App, appConfig).catch((err) => console.error(err));

    Red flag

    Says every component must be declared in an AppModule, or imports CommonModule “just in case”.

  2. J-02What kinds of data binding are there in a template? Templates

    Say this first

    Interpolation {{ value }} and property binding [prop]="expr" go from component to DOM; event binding (event)="handler($event)" goes from DOM to component; two-way binding [(value)]="sig" combines both.

    Then go deeper

    • [disabled] binds a DOM property; for things that exist only as attributes (ARIA, colspan) you use [attr.aria-label].
    • [(x)] (“banana in a box”) is sugar for [x]="v()" + (xChange)="v.set($event)". With a signal you pass the signal itself, without parentheses.
    • Template expressions should be cheap and side-effect free; a signal read like count() is fine, a heavy method call runs on every check.
    <h2>{{ user().name }}</h2>
    <img [src]="user().avatarUrl" [attr.aria-label]="user().name" />
    <button [disabled]="saving()" (click)="save()">Save</button>
    <input (input)="query.set($any($event.target).value)" />
    <app-toggle [(checked)]="notifications" />

    Red flag

    Thinks [value]="x" and value="x" are the same (the second is a static string), or writes [(checked)]="notifications()".

  3. J-03How do you write conditions and loops in a template, and why does @for need track? Templates

    Say this first

    With the built-in control flow: @if / @else, @for with @empty, and @switch / @case. track is mandatory: it tells Angular how to identify each item, so it can reuse DOM nodes instead of recreating the whole list.

    Then go deeper

    • Track by a stable unique id (track user.id); $index only for static lists; tracking the object itself means every refetch destroys and recreates the DOM.
    • It is part of the language: no NgIf/NgFor imports. @for exposes $index, $first, $last, $even, $odd, $count.
    • @if (user(); as u) gives a typed, non-null alias. Since v21.1 several @case can share a block, and @default never; (v21.2) makes a forgotten union case a compile error.
    @if (tasks().length) {
      <ul>
        @for (task of tasks(); track task.id; let i = $index) {
          <li>{{ i + 1 }}. {{ task.title }}</li>
        }
      </ul>
    } @else {
      <p>You have no tasks.</p>
    }

    Red flag

    Still reaches for *ngIf/*ngFor in new code, or cannot explain what track is for.

  4. J-04What is @let and when would you use it? Templates

    Say this first

    @let declares a local, read-only variable in the template, visible in the current block and its children. I use it to name a long expression used several times, or to unwrap a value once, e.g. @let user = user$ | async;.

    Then go deeper

    • It cannot be reassigned from the template; it is recomputed when what it reads changes.
    • It is handy inside @for for a per-item value (e.g. a final price after discount).
    • Real logic still belongs in the class as a computed(); @let is for readability, not for business rules.

    Red flag

    Uses @let as a mutable variable, or moves complex business logic into the template with it.

  5. J-05How do you add a CSS class or style conditionally? Templates

    Say this first

    With native bindings: [class.active]="isActive()" for one class, [class]="{ ... }" for several, and [style.width.px]="width()" for styles. They need no import, and static classes are combined with the bound ones, not overwritten.

    Then go deeper

    • [class] accepts a string, an array or an object; [style] accepts a string or an object, and units go in the binding (.px, .%, .rem).
    • [ngClass]/[ngStyle] are the old way; v21 has migrations ngclass-to-class and ngstyle-to-style.
    • For the component’s own host element you use the host metadata ('[class.is-loading]': 'loading()'), not @HostBinding.

    Red flag

    Builds class strings by hand in the component, or manipulates classList directly on the element.

  6. J-06What is a pipe, and what does “pure” mean? Templates

    Say this first

    A pipe transforms a value for display directly in the template, like {{ price | currency }}. By default it is pure: Angular re-runs it only when the input value or its arguments change (by reference), so it is memoised.

    Then go deeper

    • Pipes are standalone and imported like any dependency (imports: [DatePipe]); a custom one is a class with @Pipe and transform().
    • Because the check is by reference, a pure pipe does not re-run if you mutate an array in place.
    • A method called from the template runs on every check; for component-specific calculations prefer computed(), and keep pipes for reusable display formatting. Avoid pure: false.

    Red flag

    Makes a filtering pipe impure to “make it work” on a large list.

  7. J-07How does a parent pass data to a child component, and how does the child talk back? Components

    Say this first

    The child declares input() for data coming in and output() for events going out. The parent binds [value]="..." and (rate)="onRate($event)"; the child calls rate.emit(4).

    Then go deeper

    • An input() is a read-only signal: you read it with value() and derive from it with computed() — no ngOnChanges needed.
    • input.required<T>() makes a missing binding a compile error; transform: numberAttribute / booleanAttribute turn max="10" or a bare readonly attribute into the right type.
    • The child cannot set() its own input. If it must change the value, it emits an event and lets the parent decide, or uses model().
    export class StarRating {
      readonly value = input.required<number>();
      readonly max = input(5, { transform: numberAttribute });
      readonly rate = output<number>();
    
      protected readonly stars = computed(() =>
        Array.from({ length: this.max() }, (_, i) => i + 1),
      );
    }

    Red flag

    Says you need ngOnChanges to react to an input, or uses @Input/EventEmitter in new code.

  8. J-08What is model() and how is it different from input() + output()? Components

    Say this first

    model() is a writable signal that acts as an input plus an automatic xChange output, so the parent can use two-way binding: [(checked)]="notifications". When the child calls set() or update(), the parent’s signal updates.

    Then go deeper

    • Use model() when the value truly belongs to both sides, as in a toggle, a stepper or a custom input.
    • Use input() + output() when the child only proposes and the parent decides (validates, confirms, saves to the server).
    • In two-way binding you pass the signal itself (notifications), not its value (notifications()).

    Red flag

    Cannot explain what [( )] expands to, or uses model() for everything.

  9. J-09What is content projection? Components

    Say this first

    It lets a parent put its own markup between a component’s tags, and the component decides where to render it with <ng-content />. With select you can have several slots, for example a title and actions in a card.

    Then go deeper

    • select takes a tag, attribute or class selector ([card-title]); the <ng-content /> without select catches everything else, and you can put fallback content inside it.
    • Gotcha: projected content is created by the parent even if the child hides it with @if. For truly lazy content, pass an <ng-template>.
    • Projection is for flexible markup (layouts, cards, dialogs); inputs are for data the component formats itself.

    Red flag

    Passes HTML as a string input and renders it with innerHTML instead of projecting it.

  10. J-10What are signal() and computed()? Signals

    Say this first

    A signal() is a reactive value: you read it by calling it, and change it with set() or update(). A computed() is a read-only value derived from other signals; it is lazy and memoised, recalculated only when its dependencies change.

    Then go deeper

    • Dependencies are tracked automatically: whatever signals the function reads become dependencies, with no subscriptions to manage.
    • Signals compare with ===: items().push(x) notifies nobody; use update((list) => [...list, x]).
    • Rule of thumb: anything that can be calculated from other state is a computed(), not a second signal kept in sync. Expose state with asReadonly().
    export class Counter {
      protected readonly count = signal(0);
      protected readonly double = computed(() => this.count() * 2);
    
      protected increment() {
        this.count.update((c) => c + 1);
      }
    }
    // template: {{ count() }} · {{ double() }}

    Red flag

    Tries to call set() on a computed(), or forgets the parentheses and binds the signal function instead of its value.

  11. J-11What is effect(), and when should you not use it? Signals

    Say this first

    effect() runs a function once, then again whenever the signals it reads change. It is for side effects towards the outside world — localStorage, logging, a third-party library — not for computing state.

    Then go deeper

    • Do not use an effect to copy one signal into another: that is a computed() (or linkedSignal() if it must also be editable).
    • Do not fetch data from an effect: there is no cancellation and old responses can win the race. Use httpResource() / resource().
    • It must be created in an injection context (constructor or field), is destroyed with the component, and onCleanup clears timers before the next run.

    Red flag

    Uses effect() to keep fullName in sync with firstName and lastName.

  12. J-12What is the difference between the constructor and ngOnInit? How do you clean up when a component is destroyed? Components

    Say this first

    The constructor runs when the class is created and is the injection context: inject(), signals and effects go there or in field initialisers. ngOnInit runs once after the first inputs are set. For cleanup I use inject(DestroyRef).onDestroy() or takeUntilDestroyed().

    Then go deeper

    • Reading an input.required() in the constructor throws, because the parent has not bound it yet. With signal inputs you rarely need ngOnInit: a computed() or httpResource() reads the input when it is ready.
    • inject() does not work in ngOnInit or in a click handler — that is outside the injection context (error NG0203).
    • DestroyRef also works in services, directives and reusable functions, which ngOnDestroy cannot do. For DOM access after render, use afterNextRender().
    export class Clock {
      readonly now = signal(new Date());
    
      constructor() {
        const id = setInterval(() => this.now.set(new Date()), 1000);
        inject(DestroyRef).onDestroy(() => clearInterval(id));
      }
    }

    Red flag

    Says the constructor is “only for DI” and puts all logic in ngOnInit, or never cleans up intervals and subscriptions.

  13. J-13What is a service and how do you use it in a component? DI

    Say this first

    A service is a class that holds logic or state shared between components — API calls, a cart, the logged-in user. In v22 I mark it with @Service(), which is provided in root as a singleton, and get it with inject(CartService).

    Then go deeper

    • @Service() is equivalent to @Injectable({ providedIn: 'root' }): created on first injection and tree-shakable. It requires inject() — constructor DI is a compile error.
    • @Injectable is still valid (existing code, libraries for older versions). Putting the service in a component’s providers gives one instance per component instead of one per app.
    • inject() also works in functional guards, resolvers and interceptors, and avoids long super(...) calls with inheritance.
    @Service()
    export class CartService {
      private readonly _items = signal<CartItem[]>([]);
      readonly items = this._items.asReadonly();
      readonly count = computed(() => this._items().length);
    
      add(item: CartItem) {
        this._items.update((list) => [...list, item]);
      }
    }

    Red flag

    Creates the service with new CartService(), or thinks a service in providers is still the same global singleton.

  14. J-14How do you set up routing and navigate between pages? Routing

    Say this first

    You define a Routes array, register it with provideRouter(routes), and place <router-outlet /> where pages render. You navigate with routerLink in templates and router.navigate() in code; pages are usually lazy-loaded with loadComponent.

    Then go deeper

    • loadComponent: () => import('./home-page') puts the page in a separate chunk, downloaded only when the route is visited; loadChildren does the same for a whole feature.
    • Order matters — the first match wins — so the '**' wildcard goes last. routerLinkActive adds a class to the active link.
    • Use routerLink, not href: href reloads the whole application.

    Red flag

    Navigates with <a href> or window.location, or puts the wildcard route first.

  15. J-15How does a page read a route parameter like :id? Routing

    Say this first

    With withComponentInputBinding() in provideRouter, path params, query params and route data arrive as component inputs: readonly id = input.required({ transform: numberAttribute }). Because it is a signal, anything derived from it updates when the id changes.

    Then go deeper

    • Gotcha: navigating from /products/1 to /products/2 reuses the same component instance; code that reads the id once (e.g. a snapshot in ngOnInit) shows stale data.
    • Params are strings in the URL, so a transform like numberAttribute gives you a real number.
    • Since v22 child routes inherit params and data from all parents by default (paramsInheritanceStrategy: 'always').
    @Component({ templateUrl: './product-page.html' })
    export default class ProductPage {
      readonly id = input.required({ transform: numberAttribute }); // :id
      readonly tab = input<'specs' | 'reviews'>('specs');           // ?tab=
    
      readonly product = httpResource<Product>(() => '/api/products/' + this.id());
    }

    Red flag

    Only knows route.snapshot.paramMap.get('id') and does not know the component can be reused.

  16. J-16How do you call a REST API in Angular? HTTP

    Say this first

    With HttpClient, injected in a service: http.get<Product[]>('/api/products') returns an Observable. Since v21 it is available in root with no setup; provideHttpClient() is only for configuration such as interceptors.

    Then go deeper

    • The Observable is cold: no request is sent until someone subscribes (subscribe(), firstValueFrom(), the async pipe).
    • For reactive reads in a component, httpResource() gives value(), isLoading() and error() as signals and cancels old requests; for writes (POST, PUT) you call HttpClient.
    • The generic get<T> is only a type assertion, not validation. In v22 Fetch is the default backend; cross-cutting concerns (auth token, errors) go in functional interceptors.

    Red flag

    Calls http.get() without subscribing and wonders why nothing happens, or calls fetch directly from components.

  17. J-17How do you build a form with validation in Angular 22? Forms

    Say this first

    For new forms I use Signal Forms, stable in v22: a signal holds the model, form(model, schema) builds a tree of fields with rules like required(p.email), and [formField] binds each input. Reactive Forms remain the choice for existing apps full of them, or libraries that only support ControlValueAccessor.

    Then go deeper

    • The model signal is the source of truth — no internal copy, no patchValue: model.set(data) fills the form, and the form writes straight into the model.
    • Each field’s state is signals: f.email().invalid(), touched(), errors(); the whole form is a field too (f().valid(), f().submitting()).
    • Show errors only after touched(), and always validate on the server too — client validation is UX, not security.
    export class LoginForm {
      readonly model = signal({ email: '', password: '' });
    
      readonly loginForm = form(this.model, (p) => {
        required(p.email);
        email(p.email);
        minLength(p.password, 8);
      });
    }
    // template: <input [formField]="loginForm.email" />

    Red flag

    Validates by hand in the submit handler with ifs, or says Signal Forms are still only experimental in v22.

  18. J-18What does @defer do? Performance

    Say this first

    @defer lazy-loads a part of a template: the components inside it go into a separate chunk, downloaded only when a trigger fires — on idle by default, or on viewport, on interaction, when cond(). It reduces the initial bundle without touching the routes.

    Then go deeper

    • @placeholder is shown before the trigger (needed for on viewport / interaction / hover), @loading while downloading, @error if the download fails.
    • Gotcha: if a deferred component is also referenced elsewhere in the same file (outside the block), it is loaded eagerly and the chunk is lost.
    • prefetch on hover downloads early and renders later — good for actions “one click away”.
    @defer (on viewport) {
      <app-comments [postId]="post().id" />
    } @placeholder {
      <div class="comments-skeleton"></div>
    } @loading (after 100ms) {
      <app-spinner />
    }

    Red flag

    Thinks @defer is just a delayed @if and does not mention the separate JavaScript chunk.

  19. J-19How does Angular know when to update the screen? What changed in v21 and v22? Change detection

    Say this first

    Change detection re-renders templates when something notifies Angular: a signal read in the template changes, a template event fires, the async pipe emits, or markForCheck() is called. Since v21 new apps are zoneless (no zone.js), and since v22 components are OnPush by default.

    Then go deeper

    • Without zone.js, a setTimeout or subscribe() that changes a plain field does not update the UI. The fix is a signal (or toSignal() / the async pipe).
    • An OnPush component is checked only when an input gets a new reference, an event happens in it, a signal it reads changes, or it is marked for check — otherwise its subtree is skipped.
    • ChangeDetectionStrategy.Eager is the old “always check” behaviour (Default is deprecated); ng update adds it where needed.

    Red flag

    Says “zone.js detects every change automatically” for a v21+ app, or fixes missing updates by sprinkling detectChanges().

  20. J-20How do you test a component in a new Angular project? Testing

    Say this first

    Since v21 ng test runs Vitest (Karma is deprecated). I create the component with TestBed.createComponent(), set inputs, await fixture.whenStable() so the zoneless render finishes, then assert on fixture.nativeElement.

    Then go deeper

    • Test behaviour the user sees (text, clicks, emitted outputs), not private implementation details.
    • Inputs can be passed as signals with inputBinding(); outputs are spied with outputBinding() and vi.fn().
    • For HTTP, use provideHttpClientTesting() and HttpTestingController instead of a real server; services and signal stores can be tested with TestBed.inject() and no DOM.
    it('shows the right number of stars', async () => {
      const fixture = TestBed.createComponent(StarRating, {
        bindings: [inputBinding('value', () => 3)],
      });
      await fixture.whenStable();
    
      const stars = fixture.nativeElement.querySelectorAll('button');
      expect(stars).toHaveLength(5);
    });

    Red flag

    Has never written a test, or relies on fakeAsync/tick from zone.js in a zoneless Vitest project.

Mid: building features on your own

At this level the questions are about choices: computed or effect, which RxJS operator, where a provider lives, how a component is split, how you test it. A good mid answer names the option, the reason, and the case where the other option wins.

0 of 20 marked as known
  1. M-01When do you use computed() and when effect()? Why shouldn’t you set signals inside an effect? Signals

    Say this first

    computed() is for derived state: a pure, lazy, memoized value. effect() is for side effects that push signal values to the non-reactive world (DOM, localStorage, logging). Signal → signal through an effect is almost always a computed() or linkedSignal() in disguise.

    Then go deeper

    • Setting a signal in an effect means an extra run, a moment where the target is stale (one render with a wrong value) and a dependency graph that’s hard to follow.
    • Effects are not synchronous: set a signal three times and the effect runs once, with the final value. Component effects run during change detection; effects in root services run as microtasks.
    • Dependencies are dynamic: only signals read in the last run are tracked. Use untracked() for reads that shouldn’t re-trigger the effect.
    • An effect needs an injection context (constructor or field initializer) or an explicit injector; otherwise you get NG0203. Use onCleanup for timers and listeners.
    • HTTP from an effect is a smell: no cancellation, so responses can arrive out of order. That’s what resource / httpResource are for.
    readonly firstName = signal('Ana');
    readonly lastName = signal('Pop');
    
    // ✗ effect used to synchronise state
    // effect(() => this.fullName.set(this.firstName() + ' ' + this.lastName()));
    
    // ✓ derived state
    readonly fullName = computed(() => this.firstName() + ' ' + this.lastName());
    
    // ✓ effect: reactive world → outside world
    constructor() {
      effect(() => localStorage.setItem('name', this.fullName()));
    }

    Red flag

    “I use effect to keep two signals in sync” or “computed and effect are basically the same, effect just runs more often”.

  2. M-02What problem does linkedSignal() solve? Give a real example. Signals

    Say this first

    It’s a writable signal that resets from a source: derived like computed(), but you can also set() it. Typical cases: a selection that defaults to the first option of a list, a page number that goes back to 1 when filters change, an editable copy of an input.

    Then go deeper

    • It replaces the fragile signal + effect pattern: no extra run, no render with a stale value.
    • The long form (source + computation) gives you the previous value, so you can keep the user’s choice if it still exists in the new list.
    • A manual set() is valid only until the source changes again; then the computation wins.
    • Since v22.1 the set option intercepts writes, e.g. to clamp a quantity to the available stock.

    Red flag

    Copying the input into a signal in ngOnInit (it never resets) or syncing with an effect.

  3. M-03How do resource / httpResource work, and why are they better than subscribing to HTTP inside an effect? HTTP

    Say this first

    A resource turns async loading into signals: a reactive params (or URL) function, and when it changes the old request is cancelled and a new one starts. You get value(), status(), isLoading(), error() and reload() for free, with no race conditions.

    Then go deeper

    • resource gets an abortSignal in the loader; httpResource goes through HttpClient, so interceptors apply; rxResource takes an Observable stream and behaves like switchMap.
    • Returning undefined from params means status idle: the loader doesn’t run. In v22 you can also throw ResourceParamsStatus.IDLE / LOADING, and chain resources with ctx.chain().
    • hasValue() is a type guard; defaultValue removes undefined; parse validates or maps the response (e.g. DTO → model, Zod).
    • Resources are for reading. Writes (POST/PUT/DELETE) go through HttpClient, then you update() the resource locally (optimistic) or reload() it.
    readonly userId = input.required<number>();
    
    // a new id → the old request is cancelled, a new one starts
    readonly user = httpResource<User>(() => '/api/users/' + this.userId());
    
    // template:
    // @if (user.isLoading()) { <app-spinner /> }
    // @if (user.error()) { <button (click)="user.reload()">Retry</button> }
    // @else if (user.hasValue()) { <h2>{{ user.value().name }}</h2> }

    Red flag

    Using httpResource for a POST on button click, or not knowing what happens to an in-flight request when the params change.

  4. M-04How do you implement a search-as-you-type without firing a request on every keystroke? Signals

    Say this first

    In v22 I wrap the query signal with debounced(query, 300) and feed its value() into an httpResource. The debounce removes the noise; the resource cancels stale requests, so the last query always wins.

    Then go deeper

    • debounced() returns a Resource: value() updates after the last change, and isLoading() is true while the user is still typing.
    • It’s experimental in v22. The classic alternative is toSignal(toObservable(query).pipe(debounceTime(300), distinctUntilChanged())): three conversions for one debounce.
    • wait can be a function, e.g. no delay when the field is cleared.
    • In Signal Forms there’s a dedicated debounce(path.field, 300) rule, and validateHttp has its own debounce option.
    import { httpResource } from '@angular/common/http';
    import { debounced, signal } from '@angular/core';
    
    export class UserSearch {
      readonly query = signal('');
      readonly search = debounced(this.query, 300);
    
      readonly users = httpResource<UserDto[]>(
        () => '/api/users?q=' + encodeURIComponent(this.search.value()),
        { defaultValue: [] },
      );
    }

    Red flag

    A setTimeout in the input handler with a manual subscribe() and no cancellation of the previous request.

  5. M-05How do you combine signals and RxJS in a component without leaking subscriptions? When do you keep RxJS? RxJS

    Say this first

    Signals for the displayed state, RxJS at the edges (events, streams, complex async). toSignal() subscribes and unsubscribes automatically; for a manual subscribe() I add takeUntilDestroyed(), which ties it to the component’s DestroyRef.

    Then go deeper

    • toSignal() without initialValue is typed T | undefined; requireSync: true works only if the source emits synchronously (e.g. a BehaviorSubject).
    • toObservable() emits asynchronously (through an effect) and only the last value if the signal is set several times in the same tick — it’s not an event stream.
    • takeUntilDestroyed() without arguments works only in an injection context; elsewhere pass inject(DestroyRef) explicitly. For non-RxJS cleanup use DestroyRef.onDestroy().
    • Keep RxJS for websockets, combining event streams, retry/backoff, and flattening policies (switchMap, exhaustMap…). Signals have no notion of time.

    Red flag

    “Signals replace RxJS completely” — or the opposite: subscribing everywhere and copying values into fields with no unsubscription.

  6. M-06Explain switchMap vs mergeMap vs concatMap vs exhaustMap, with a use case for each. RxJS

    Say this first

    They differ in what happens when a new value arrives while the previous inner Observable is still running: switchMap cancels it (search), mergeMap runs in parallel (uploads), concatMap queues in order (ordered saves), exhaustMap ignores the new one (submit, login).

    Then go deeper

    • Choosing the operator is choosing a concurrency policy; the wrong one causes real bugs: double orders with mergeMap on a pay button, lost saves with switchMap on autosave.
    • mergeMap accepts a concurrency limit, e.g. mergeMap(upload, 3).
    • Put catchError inside the inner pipe: after the flattening operator, the first error completes the outer stream and the search “dies”.
    • With signals, httpResource and rxResource already give you switchMap semantics for reads.
    // the last one wins; errors handled INSIDE, so the stream survives
    query$.pipe(
      debounceTime(300),
      switchMap((q) => this.api.search(q).pipe(catchError(() => of([])))),
    );
    
    files$.pipe(mergeMap((file) => this.api.upload(file), 3)); // parallel, max 3
    patches$.pipe(concatMap((patch) => this.api.save(patch))); // in order
    submit$.pipe(exhaustMap(() => this.api.login(this.credentials()))); // ignore double clicks

    Red flag

    “I always use switchMap” or nested subscribe() calls instead of a flattening operator.

  7. M-07In a zoneless Angular 22 app, what triggers change detection, and why might a field updated in setTimeout not show up? Change detection

    Say this first

    Without zone.js nothing patches timers or promises. Change detection is scheduled by a signal read in the template changing, a template/host event, the async pipe, markForCheck(), setInput() or a navigation. A plain field set in setTimeout or subscribe() notifies nobody — make it a signal.

    Then go deeper

    • Since v22 components without changeDetection are OnPush; the old behaviour is ChangeDetectionStrategy.Eager (Default is deprecated). ng update adds Eager where needed.
    • An OnPush component is checked only on a new input reference (===), an event in it or its children, a signal read in its template, the async pipe, or markForCheck(). Otherwise the whole subtree is skipped.
    • Mutating an array passed as input (push) doesn’t change the reference: use immutable updates.
    • markForCheck() is the emergency fix for legacy callbacks; detectChanges() is synchronous and rarely needed.
    // ✗ works with zone.js, NOT zoneless + OnPush
    // now = new Date();
    // setInterval(() => (this.now = new Date()), 1000);
    
    // ✓ a signal schedules change detection by itself
    export class Clock {
      readonly now = signal(new Date());
    
      constructor() {
        const id = setInterval(() => this.now.set(new Date()), 1000);
        inject(DestroyRef).onDestroy(() => clearInterval(id));
      }
    }

    Red flag

    Calling detectChanges() everywhere, or believing OnPush means “the component never updates unless inputs change”.

  8. M-08How do viewChild() and contentChildren() differ from the old decorators, and when can you safely read them? Components

    Say this first

    Signal queries return signals, so results are reactive: you derive from them with computed() instead of ngAfterViewInit and QueryList.changes. The view doesn’t exist in the constructor, so DOM work goes in afterNextRender(); viewChild.required() throws if read too early.

    Then go deeper

    • viewChild looks in the component’s own template; contentChild looks in what the parent projected (e.g. a Tabs container discovering its Tab children).
    • contentChildren looks only at direct children by default; use descendants: true for deeper matches.
    • read picks what you get: ElementRef, TemplateRef, ViewContainerRef, a directive, or (v22.2) the child’s Injector.
    • Gotcha: querying a component by type in the class makes it eager, so it won’t be split out by a @defer block.

    Red flag

    Reading a view query in the constructor or ngOnInit and being surprised it’s undefined.

  9. M-09Content projection with ng-content vs passing an ng-template rendered with ngTemplateOutlet: when do you choose which? Templates

    Say this first

    ng-content is for static slots: the parent creates the content once, whether or not the child shows it. An ng-template is a recipe the child instantiates when and as often as it wants, with a context — needed for lazy content and for “how should a row look” APIs.

    Then go deeper

    • Wrapping ng-content in @if hides the DOM, but the projected components already exist and their lifecycle already ran.
    • Named slots use select (tag, attribute, class); ngProjectAs projects several elements into one slot without a wrapper.
    • The context passes data back: $implicit maps to let-item. Without a directive with ngTemplateContextGuard, the let- variables are any.
    • A template sees the injector where it was declared; since v21.2 [ngTemplateOutletInjector]="'outlet'" makes it use the injector where it’s rendered.
    @Component({
      selector: 'app-collapsible',
      imports: [NgTemplateOutlet],
      template: `
        <button (click)="open.set(!open())">Toggle</button>
        @if (open()) { <ng-container [ngTemplateOutlet]="content() ?? null" /> }
      `,
    })
    export class Collapsible {
      readonly content = contentChild(TemplateRef); // created only when opened
      readonly open = signal(false);
    }
    // <app-collapsible><ng-template><app-heavy-report /></ng-template></app-collapsible>

    Red flag

    Thinking @if around ng-content makes the projected content lazy.

  10. M-10How would you build a reusable, type-safe data table component without turning it into a “god component”? Components

    Say this first

    Make the component generic (DataTable<T>): T is inferred from rows, column keys are restricted to keyof T, outputs are typed. Customisation goes through cell templates, not boolean inputs, and domain-specific wrappers live in the feature.

    Then go deeper

    • The god component smell: any[] data, isProductTable flags, 30 inputs and domain knowledge in shared/.
    • Cell templates are found with contentChildren() on a directive; ngTemplateContextGuard types let-row as T instead of any.
    • Pass behaviour as functions (trackBy, displayWith, compareWith) so the component doesn’t need to know the shape of the data.
    • A typo in a column key becomes a compile error, not an empty column in production.

    Red flag

    Adding another boolean input every time a page needs something different.

  11. M-11How do you render a component chosen at runtime, e.g. dashboard widgets from a config? Components

    Say this first

    Climb a ladder: a small known set → @switch; the component is a value → NgComponentOutlet with inputs; full control (outputs, directives) → ViewContainerRef.createComponent() with bindings (inputBinding, outputBinding, twoWayBinding).

    Then go deeper

    • A registry of () => import(…) loaders makes each widget a separate lazy chunk.
    • inputBinding('title', () => …) is reactive, unlike the old setInput() that you had to call again on every change, plus manual output subscriptions.
    • ComponentFactoryResolver was removed in v22: you pass the class directly to createComponent.
    • For toasts and dialogs outside the tree, use the standalone createComponent() with an EnvironmentInjector, then appRef.attachView() so it joins change detection, and destroy it yourself.
    private readonly vcr = inject(ViewContainerRef);
    
    private async render(type: WidgetType) {
      const component = await WIDGET_REGISTRY[type](); // lazy chunk
      this.vcr.clear();
      this.vcr.createComponent(component, {
        bindings: [
          inputBinding('title', () => this.config().title), // stays in sync
          outputBinding('remove', () => this.removed.emit(this.config().id)),
        ],
      });
    }

    Red flag

    Reaching for ComponentFactoryResolver or innerHTML with component tags.

  12. M-12How do you manage the state of a feature page without a state library? Where do smart and dumb components fit in? Architecture

    Say this first

    A signal-based store service: private writable signals, public read-only signals (asReadonly()) and computed() selectors, plus methods with domain names doing immutable updates. The routed (smart) page provides and injects the store; dumb components get data through inputs and emit intentions through outputs.

    Then go deeper

    • Scope by lifetime: feature state in providers of the page or route (dies with it), global state (user, cart, theme) in a root @Service().
    • Server data goes into httpResource inside the store; state that must survive a refresh or be shareable goes into the URL (query params + input()).
    • A dumb component may have local UI state and inject UI things (ElementRef, DestroyRef), but never data services or the router — otherwise it can’t be reused or tested easily, and a list of 50 cards makes 50 requests.
    • NgRx SignalStore becomes worth it when the team wants conventions, plugins and devtools, not by default.

    Red flag

    Putting every piece of state in one global root service, or exposing writable signals that any component mutates in place.

  13. M-13Explain the DI hierarchy: when do you provide in root, on a route or on a component, and when do you need an InjectionToken? DI

    Say this first

    inject() looks up the element injectors (current element, then DOM parents), then the route injector, root and platform, and throws NG0201 if nothing matches. Root (@Service()) means one app-wide singleton; route providers mean one instance per feature; component providers mean one instance per component and its children. Tokens are for values that aren’t classes: config, URLs, functions.

    Then go deeper

    • @Service() (v22) equals providedIn: 'root', is tree-shakable and allows only inject(). autoProvided: false forces you to list it in providers.
    • inject() beats constructor DI: no super(…) chains, it works in guards, interceptors and factories, and enables reusable injectX() functions. It needs an injection context (NG0203 otherwise).
    • useClass, useValue, useFactory, useExisting and multi: true for plugin-style arrays; optional, self, skipSelf, host modify the lookup.
    • viewProviders are invisible to projected content; EnvironmentProviders (provideX()) can go only at app or route level, not in a component.

    Red flag

    Listing a root service in a component’s providers “to be safe” — creating a second instance and hard-to-find state bugs.

  14. M-14Functional guards and resolvers: what’s the difference between canMatch and canActivate, and when would you avoid a resolver? Routing

    Say this first

    Guards are plain functions using inject(). canMatch decides whether the route “exists” for the user — if not, the router tries the next route and the lazy chunk isn’t downloaded. canActivate blocks entry to a route that matched. A resolver delays navigation until data arrives; for lists and dashboards I prefer httpResource in the page.

    Then go deeper

    • Return true/false, a UrlTree, or a RedirectCommand (redirect + navigation options like replaceUrl); since v22.2 you can also throw a RedirectCommand.
    • Resolver: the page appears with data ready, but navigation waits and an error cancels it. httpResource: instant navigation, loading and error are UI state.
    • With withComponentInputBinding() resolved data, params and query params arrive as input()s.
    • Guards are UX, not security: the API must enforce authorization too. canDeactivate protects unsaved changes.

    Red flag

    Class-based guards with constructor injection as the default, or treating a guard as the only access control.

  15. M-15Write a functional HTTP interceptor that adds an auth token. What are the gotchas? HTTP

    Say this first

    An HttpInterceptorFn is a function (req, next) that returns next(req) — or a clone of the request, because requests are immutable. It’s registered with provideHttpClient(withInterceptors([…])) and can use inject().

    Then go deeper

    • Only attach the token to your own API (check the URL), never to third-party hosts.
    • Order matters: requests go through the interceptors in the listed order, responses come back in reverse.
    • Call inject() at the top of the function, not inside RxJS callbacks like catchError that run later, outside the injection context.
    • Use HttpContextToken for per-request metadata (e.g. skip the loading bar) instead of fake headers. httpResource and validateHttp also go through interceptors.
    export const authInterceptor: HttpInterceptorFn = (req, next) => {
      const token = inject(AuthStore).token();
      if (!token || !req.url.startsWith('/api')) return next(req);
    
      return next(req.clone({ setHeaders: { Authorization: 'Bearer ' + token } }));
    };
    
    // app.config.ts
    provideHttpClient(withInterceptors([authInterceptor, errorInterceptor]));

    Red flag

    Mutating req.headers directly, or sending the token to every URL.

  16. M-16How do you structure error handling in an Angular app: HTTP errors, rendering errors and everything else? Architecture

    Say this first

    In layers: predictable HTTP errors (404, 422) are UI state in the store or component (resource.error()); cross-cutting ones (401, 5xx) go in an interceptor; a broken area of the page is isolated with @boundary; anything uncaught ends up in a custom global ErrorHandler for logging and telemetry.

    Then go deeper

    • @boundary (v22.2, developer preview) has @error (let e = $error; let reset = $reset) blocks, optionally with when conditions — the first match wins.
    • @boundary catches rendering errors only, not errors from event handlers or async code — those still reach the ErrorHandler.
    • provideBrowserGlobalErrorListeners() forwards window.onerror and unhandled rejections to the ErrorHandler.
    • An interceptor should rethrow (throwError(() => err)) after its cross-cutting reaction, so the caller can still react. Validation errors are not exceptions: they live in the form’s errors().
    @boundary {
      <app-price-widget [price]="price()" />
      <app-recommendations />
    } @error (let e = $error; when e instanceof PermissionError) {
      <p>You don't have access to this widget.</p>
    } @error (let e = $error; let retry = $reset) {
      <app-error-card [message]="e.message" (retry)="retry()" />
    }

    Red flag

    A try/catch with console.log in every component, or swallowing errors in an interceptor so nobody else sees them.

  17. M-17How does validation work in Signal Forms, including async validation, and how is it different from typed Reactive Forms? Forms

    Say this first

    In Signal Forms the model is a plain signal, and form(model, schema) builds a field tree over it with rules declared in a schema function (required, email, validate, validateHttp…). Every field state is a signal: valid(), errors(), pending(), touched(). Reactive Forms keep their own copy of the value inside FormControls and expose changes as Observables.

    Then go deeper

    • No internal copy: model.set() updates the inputs and typing updates the model — no patchValue / valueChanges.
    • Cross-field rules use valueOf() in validate; conditional ones use when, hidden, disabled (hidden and disabled fields aren’t validated); reusable rules use schema() + apply().
    • validateHttp goes through HttpClient (so interceptors apply) and has a debounce option in v22; validateAsync works with any resource. pending() is true while it runs.
    • Server errors from submission.action can be returned with a fieldTree so they land on the right field.
    • Migration can be gradual with SignalFormControl and compatForm; typed Reactive Forms (nonNullable, getRawValue()) remain valid for existing code.
    readonly model = signal({ username: '', password: '' });
    
    readonly signup = form(this.model, (p) => {
      required(p.username);
      minLength(p.password, 8, { message: 'At least 8 characters' });
      validateHttp(p.username, {
        request: ({ value }) => '/api/username-available?username=' + value(),
        debounce: 400,
        onSuccess: (res: { available: boolean }) =>
          res.available ? undefined : { kind: 'taken', message: 'Username taken' },
        onError: () => ({ kind: 'network', message: 'Could not verify' }),
      });
    });

    Red flag

    Writing an async validator that fires a request on every keystroke without debounce, or not knowing the field is pending meanwhile.

  18. M-18How do you build a custom form control (e.g. a star rating) that works with Signal Forms? And with Reactive Forms? Forms

    Say this first

    For Signal Forms you implement FormValueControl<T>: the only required member is value = model(); then [formField] works on it exactly like on an input. For Reactive Forms the classic way is ControlValueAccessor registered with NG_VALUE_ACCESSOR.

    Then go deeper

    • Optional inputs like disabled, invalid, touched are filled in automatically by the form — declare only what you use. In v22 a touch output reports that the user touched the control.
    • Boolean controls implement FormCheckboxControl with checked instead of value.
    • A CVA needs four methods (writeValue, registerOnChange, registerOnTouched, setDisabledState) and you must call onChange yourself; forgetting onTouched breaks “show errors after touch”.
    • Interop in v22: Signal Forms can bind CVAs, and template/reactive forms support FormValueControl, so one control can serve both during migration.

    Red flag

    Passing a FormControl as an input to the child component and mutating it there.

  19. M-19How does @defer work, which triggers would you use, and what makes a deferred block not actually lazy? Performance

    Say this first

    @defer moves the block’s components into a separate chunk loaded on a trigger: on idle (default), viewport, interaction, hover, timer, immediate or when a condition. prefetch downloads earlier than it renders. It only stays lazy if those dependencies are standalone and used nowhere else in the file.

    Then go deeper

    • Referencing a deferred component in the class (e.g. viewChild(HeavyChart)) or elsewhere in the template makes it eager.
    • @placeholder shows before the trigger (and is the observed element for viewport / interaction / hover without a target); @loading (after 100ms; minimum 500ms) avoids flicker; @error handles a failed chunk download.
    • when fires only once; several triggers are combined with OR.
    • New: on viewport({ rootMargin: '200px' }) (v21), on idle(500) (v22). With SSR, hydrate on … triggers give incremental hydration, enabled by default in v22.
    @defer (on viewport; prefetch on idle) {
      <app-comments [postId]="post().id" />
    } @placeholder (minimum 300ms) {
      <div class="comments-skeleton"></div>
    } @loading (after 100ms; minimum 500ms) {
      <app-spinner />
    } @error {
      <p>We couldn't load the comments.</p>
    }
    
    @defer (on interaction; prefetch on hover) {
      <app-share-dialog />
    } @placeholder { <button>Share</button> }

    Red flag

    Wrapping above-the-fold content in @defer, or thinking @defer delays an HTTP request rather than loading code.

  20. M-20How do you test a component with signal inputs and a service that calls HTTP, using Vitest in Angular 21+? Testing

    Say this first

    Components: TestBed.createComponent(Cmp, { bindings: [inputBinding('value', signal)] }), then await fixture.whenStable(), since there’s no zone to flush. HTTP: provideHttpClient() + provideHttpClientTesting(), then HttpTestingController.expectOne(url).flush(data) and verify() in afterEach.

    Then go deeper

    • Vitest is the default runner since v21 (@angular/build:unit-test); Karma is deprecated. Use vi.fn() for output spies and vi.useFakeTimers() instead of fakeAsync.
    • Changing the bound signal updates the input like in a real parent — no setInput() needed.
    • Signal stores are the easiest to test: TestBed.inject(), call methods, assert on computed()s. Effects run asynchronously, so flush them with TestBed.tick().
    • Test behaviour through the DOM or harnesses, not private fields. TestBed.createDirective() (v22.2) tests a directive without a hand-written host.
    beforeEach(() => {
      TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
      api = TestBed.inject(ProductsApi);
      http = TestBed.inject(HttpTestingController);
    });
    
    afterEach(() => http.verify()); // no unexpected requests
    
    it('maps DTO → Product', async () => {
      const result = firstValueFrom(api.get(1));
      http.expectOne('/api/products/1').flush({ id: 1, name: 'Mouse', price: 99, stock: 0 });
      expect(await result).toEqual({ id: 1, name: 'Mouse', price: 99, inStock: false });
    });

    Red flag

    Hitting the real backend in unit tests, or calling detectChanges() repeatedly and asserting on private fields.

Senior: internals, architecture, trade-offs

Senior interviews check whether you can decide for a team: how the framework works underneath, how an app stays maintainable at 50 developers, what SSR, security and performance cost. Always answer with a position, the trade-off, and how you would introduce it in a real project.

0 of 20 marked as known
  1. S-01How do signals work internally? Explain the producer/consumer graph, push-pull and why they are glitch-free. Signals

    Say this first

    Signals form a reactive graph built from reads: signal() is a producer, computed() is both, effects and templates are live consumers. A write only bumps a version and pushes a “maybe dirty” notification; values are pulled and recomputed lazily when someone reads, so a computed always sees consistent inputs and runs once per batch of writes.

    Then go deeper

    • Dependencies are dynamic: on every run the producer list is rebuilt from what was actually read, so a signal read only in an inactive if branch does not trigger anything.
    • Equality stops propagation: the version increases only if the new value is not equal (Object.is by default). A computed whose result does not change does not wake its consumers; { equal } customises this for values recreated from the server.
    • Consequence: mutation (items().push()) notifies nobody, because the reference and the version stay the same. Immutability is not style, it is how the graph detects change.
    • Effects are asynchronous and scheduled with change detection (view effects before their component is checked, root effects before all components). Signals are not an event mechanism and do not order side effects in time — that is RxJS territory.
    const a = signal(1);
    const b = signal(2);
    const sum = computed(() => a() + b());
    const product = computed(() => a() * b());
    const result = computed(() => sum() + product()); // diamond
    
    a.set(5);
    b.set(3);  // push: versions bump, nothing is recalculated yet
    result();  // pull: sum, product, result run once each, with consistent values
    
    // equality cutoff: a new array with the same content does not propagate
    const ids = signal<number[]>([], {
      equal: (x, y) => x.length === y.length && x.every((v, i) => v === y[i]),
    });

    Red flag

    “A signal is just a BehaviorSubject with a nicer API” or “set() re-renders the component synchronously” — it shows no understanding of laziness, versions and push-pull.

  2. S-02In a zoneless application with OnPush components, what actually schedules change detection, and what does Angular skip? Change detection

    Say this first

    Without zone.js nobody patches setTimeout or promises, so Angular renders only when it is told: a signal read in a template changes, a template/host listener fires, the async pipe emits, markForCheck(), setInput() or a navigation. An OnPush subtree is skipped unless one of those marks it, so the work is proportional to what changed, not to the size of the tree.

    Then go deeper

    • The template is a reactive consumer: when a signal it read changes, Angular marks that view for refresh (like markForCheck) and change detection reaches only the affected components.
    • OnPush compares inputs by reference (===): mutating an object passed through an input does not update the child — the same immutability rule as for signals.
    • The classic bug: a plain field set in setTimeout, subscribe() or a third-party callback (WebSocket, Maps) — “it only updates after I click somewhere”. The fix is a signal or toSignal(); markForCheck() is the escape hatch for legacy code.
    • detectChanges() is synchronous and rarely needed (measurements, tests); markForCheck() is the recommended asynchronous variant.
    // ✗ worked with zone.js, does nothing zoneless + OnPush
    export class Clock {
      now = new Date();
      constructor() {
        setInterval(() => (this.now = new Date()), 1000);
      }
    }
    
    // ✓ the signal tells Angular what changed
    export class Clock {
      readonly now = signal(new Date());
      constructor() {
        const id = setInterval(() => this.now.set(new Date()), 1000);
        inject(DestroyRef).onDestroy(() => clearInterval(id));
      }
    }

    Red flag

    Sprinkling detectChanges() everywhere until the screen updates, or believing zoneless means “Angular checks less often, so it is buggier”.

  3. S-03How would you migrate a large, zone.js-based application to zoneless without stopping feature work? Change detection

    Say this first

    Incrementally: first make components correct under OnPush with state in signals, while zone.js is still there as a safety net, then remove zone.js once nothing relies on it. The risk is not Angular, it is code and libraries that silently relied on zone.js to trigger renders.

    Then go deeper

    • Inventory first: plain fields set in async callbacks, subscribe() into fields, libraries waiting for NgZone.onStable. Angular 21 ships an analysis tool (onpush_zoneless_migration, through the CLI MCP server, ng mcp).
    • Migrate leaf components first, feature by feature: remove Eager, move state to signals / toSignal(), review and test. Each PR is small and reversible.
    • Until the end, keep provideZoneChangeDetection({ eventCoalescing: true }) and the zone.js polyfill; then drop both and watch for “updates only after a click” bugs.
    • Tests change too: fakeAsync needs zone.js, so move to await fixture.whenStable() and Vitest fake timers.
    • In a team: a lint/review rule that new code uses signals for template state, and a short checklist in the PR template, so the migrated area does not regress.

    Red flag

    “Remove zone.js in one big PR and fix whatever breaks in QA” — no inventory, no safety net, no plan for libraries and tests.

  4. S-04How do you design the state layer of a large app: a service with signals, NgRx SignalStore or classic NgRx Store? Architecture

    Say this first

    The first question is not “which library” but where each piece of state lives and who owns it. A service with private signals, computed() selectors and named methods covers most apps; SignalStore when you want an imposed shape, entities and reusable features across many teams; classic NgRx Store only when you truly need an action log, time-travel or event-driven flows.

    Then go deeper

    • Keep state at the lowest level that needs it (component → feature → global) and lift it only when a new consumer appears. Page state is provided on the route or component, not in root, so it does not “stick” between visits.
    • Server data is a cache, not your state: keep it in httpResource/resource and reload after writes. The URL is state too (filters, pagination).
    • Rules that scale: expose read-only signals (asReadonly()), one source of truth with the rest derived, no stores injecting each other circularly — that usually signals a missing facade or a misplaced concept.
    • With SignalStore, only methods call patchState, rxMethod handles async flows (with catchError inside the inner stream) and signalStoreFeature removes copy-pasted loading/error code. The libraries can coexist with an existing NgRx Store.

    Red flag

    “Always NgRx for anything serious” or “one global store for the whole app” — tooling chosen before understanding ownership and lifetime of the state.

  5. S-05How do you structure a large codebase into feature and shared code, and how do you enforce the boundaries in a monorepo? Architecture

    Say this first

    Organise by domain, not by file type: features depend on core and shared, shared depends on nothing domain-specific, and features never import each other directly. Rules that live only in a wiki break in the first week, so I put them in lint — no-restricted-imports in a single app, or Nx tags with @nx/enforce-module-boundaries in a monorepo.

    Then go deeper

    • Tags on two dimensions: scope (who talks to whom: scope:shop, scope:shared) × type (dependency direction: feature → ui / data-access → util). Constraints from both apply with AND.
    • One public API per feature/library (a single barrel at the boundary, never in every folder); deep imports and static imports of lazy libraries are violations.
    • Granularity is a trade-off: small libraries by domain and type give narrow nx affected graphs and cache hits; one giant shared library makes every PR affect the whole repo.
    • Move to shared only what is generic, stable and already used in several features (“rule of three”); domain components used elsewhere stay in their owning feature and are exported through its public API.
    '@nx/enforce-module-boundaries': ['error', {
      depConstraints: [
        // scope: who may talk to whom
        { sourceTag: 'scope:shop', onlyDependOnLibsWithTags: ['scope:shop', 'scope:shared'] },
        { sourceTag: 'scope:shared', onlyDependOnLibsWithTags: ['scope:shared'] },
        // type: which way dependencies flow
        { sourceTag: 'type:feature', onlyDependOnLibsWithTags: ['type:feature', 'type:ui', 'type:data-access', 'type:util'] },
        { sourceTag: 'type:ui', onlyDependOnLibsWithTags: ['type:ui', 'type:util'] },
        { sourceTag: 'type:util', onlyDependOnLibsWithTags: ['type:util'] },
        // ui never reaches data-access, not even transitively
        { sourceTag: 'type:ui', notDependOnLibsWithTags: ['type:data-access'] },
      ],
    }],

    Red flag

    Folders by type (components/, services/) and a shared junk drawer, with boundaries “agreed verbally”.

  6. S-06How would you apply ports & adapters in Angular? Where does HTTP live, and when is it not worth it? Architecture

    Say this first

    The domain is plain TypeScript, use cases depend on ports written as abstract classes (which are also DI tokens), and HTTP lives only in adapters at the edge, which map DTOs to entities. The only place that binds a port to an adapter is the composition root — app.config.ts or a route’s providers. For plain CRUD screens it is overkill.

    Then go deeper

    • Abstract class over interface: an interface disappears at compile time, so you would need a separate InjectionToken; the abstract class is both the type and the token.
    • Errors are part of the contract: the adapter translates timeouts/503 into a domain error, so no component ever sees HttpErrorResponse.
    • The payoff is in testing: domain rules as pure functions, use cases with an in-memory adapter, and one contract test suite run against every adapter.
    • Pragmatic variant: a pure domain for rules that matter, a port only where there are (or will be) several implementations, the rest in the usual data-access/ui structure — and enforce the layers with lint.
    // application/task-repository.ts — the PORT (type + DI token)
    export abstract class TaskRepository {
      abstract list(): Promise<readonly Task[]>;
      abstract save(task: Task): Promise<void>;
    }
    
    // infrastructure/http-task-repository.ts — the ADAPTER (HttpClient + DTO mapping)
    @Injectable()
    export class HttpTaskRepository extends TaskRepository { /* … */ }
    
    // app.config.ts — the COMPOSITION ROOT
    providers: [{ provide: TaskRepository, useClass: HttpTaskRepository }]

    Red flag

    Components injecting HttpClient or the concrete adapter directly, DTOs flowing into templates, or four layers for two forms “because Clean Architecture”.

  7. S-07When would you choose micro-frontends with Native Federation, and when would you push back? Architecture

    Say this first

    Micro-frontends solve an organisational problem — autonomous teams that must deploy independently and today block each other at release — not a technical one. If I cannot name two teams that block each other, I recommend one app in a monorepo with lazy routes and enforced boundaries; MFE makes performance worse and only moves tangled code between repositories.

    Then go deeper

    • Valid reasons: many teams with different cadences, very large organisations, a strangler-fig migration from a legacy app, separate products under one experience.
    • Angular must be a singleton with strictVersion, ideally the exact same version everywhere: two runtimes mean components that cannot enter the shell’s tree and services with two instances. A remote on another major is isolated as a Web Component or iframe.
    • Treat each remote’s exposes as a semver public API; communicate via the URL, then typed events, then a few shared singleton services — never a shared global store (that is a distributed monolith).
    • Plan the costs from day one: a timeout and fallback per remote, a manifest per environment, the remote name and version in every logged error, e2e on the production combination.

    Red flag

    “We need micro-frontends for scalability and performance” or setting strictVersion: false to make version warnings go away.

  8. S-08When is Angular Elements the right tool, and what are the pitfalls of shipping an Angular component as a custom element? Components

    Say this first

    When a component must live outside an Angular app: CMS widgets, client websites, a design system for other frameworks, or a remote that cannot share the Angular version. createCustomElement() turns inputs into attributes/properties and outputs into CustomEvents, but each bundle brings its own runtime (≈ 33 kB transferred) and there is no shared DI — inside the same Angular app it is almost always the wrong choice.

    Then go deeper

    • Attributes are always strings: numeric/boolean inputs need numberAttribute / booleanAttribute, and objects/arrays go only through properties.
    • Events do not bubble and are not composed: listen on the element itself. Use specific output names (ratingChange, not change).
    • Use ViewEncapsulation.ShadowDom so the host page’s CSS does not leak in; theme through CSS variables and ::part().
    • Group a team’s widgets into one bundle (one createApplication(), several define() calls), use a tag different from the component selector, and do not reuse a destroyed element — create a new one.

    Red flag

    Using Angular Elements to reuse components between screens of the same Angular app, or for SEO-critical content that should be rendered on the server.

  9. S-09How do you decide between CSR, SSR and prerender in an Angular app, and what does it cost to operate? SSR

    Say this first

    Per route, not per project: app.routes.server.ts sets RenderMode.Prerender, Server or Client for each path. Prerender stable public pages, render on the server public pages with fresh data, and keep the area behind the login on the client. SSR improves LCP and SEO; it does not shrink the JavaScript or fix a slow app after hydration.

    Then go deeper

    • Prerender with parameters needs getPrerenderParams; PrerenderFallback.Server (the default) lets you prerender the top N products and render the rest on request. Every URL is a full render at build time.
    • Server mode means a Node process: TTFB is at least the slowest API called during rendering, CPU per request, and module-level state shared between users.
    • Correct status codes matter for SEO: set 404 through RESPONSE_INIT instead of a soft 404; caching is per route — public, s-maxage only for identical content, private, no-store for personal HTML.
    • In v22 configure allowedHosts (or NG_ALLOWED_HOSTS): an unknown host gets 400 as SSRF protection. If nobody can operate a Node server, use Prerender with outputMode: 'static'.

    Red flag

    “Turn on SSR everywhere, it makes the app faster” — ignoring that TTI barely changes, the server cost, and personal pages cached publicly at the CDN.

  10. S-10Explain incremental hydration and event replay in v22. How do you use them on a product page? SSR

    Say this first

    In v22 a plain provideClientHydration() enables incremental hydration, and with it event replay. @defer (hydrate on …) blocks are rendered on the server but stay dehydrated in the browser — their JS is not even downloaded — until the trigger fires; clicks made before that are queued and replayed, and a click inside a dehydrated block hydrates it.

    Then go deeper

    • Unlike a regular @defer, the final content is already on screen, so you can defer above the fold without layout shift.
    • hydrate never is only for truly static content: it is not annotated, so nothing is replayed. Keep the @placeholder: hydrate triggers are ignored on client-side navigation.
    • withIncrementalHydration() is deprecated and withEventReplay() is redundant with the default; the opt-out is withNoIncrementalHydration().
    • Do not wrap components of a few KB: the extra chunk and request cost more than they save. Use CSS states (:active, :focus-visible) so users see that the click registered.
    <app-product-header [product]="product()" />  <!-- hydrated at bootstrap -->
    
    @defer (hydrate on interaction) {
      <app-gallery [images]="product().images" />  <!-- JS only at the first click -->
    } @placeholder {
      <div class="gallery-skeleton"></div>         <!-- shown on client-side navigation -->
    }
    
    @defer (hydrate on viewport) {
      <app-reviews [productId]="id()" />           <!-- below the fold -->
    } @placeholder {
      <div class="reviews-skeleton"></div>
    }
    
    @defer (hydrate never) { <app-footer /> }      <!-- static HTML forever -->

    Red flag

    Putting hydrate never on something with listeners, or removing @placeholder “because it is never visible”.

  11. S-11What code is not SSR-safe, and how do you prevent hydration mismatches in a team? SSR

    Say this first

    Anything touching window, localStorage, layout or observers in constructors, field initialisers, ngOnInit or effect() — those run on the server too. Browser-only work goes into afterNextRender / afterRenderEffect or behind a platform provider, and the rendered output must be identical on both platforms.

    Then go deeper

    • @if (isPlatformBrowser(…)) in a template does not throw, but Angular throws away the server DOM for that block and rebuilds it — a layout shift. Prefer @defer or afterNextRender.
    • Mismatch causes: direct DOM manipulation (innerHTML, appendChild), invalid HTML (<div> inside <p>), third-party scripts, a CDN stripping comments (NG0507), and values like Date.now() that differ between platforms.
    • Zoneless servers do not see your own async (fetch, SDKs, timers): use resource() or PendingTasks, otherwise the HTML leaves empty and the browser requests the data again.
    • State leaks: module-level variables and useValue objects are shared by all users of the Node process; use services or useFactory.
    • In a team: an SSR build in CI, a review checklist, and ngSkipHydration allowed only as a documented, temporary patch.

    Red flag

    if (typeof window !== 'undefined') sprinkled through components, or treating effect() as a “browser-only hook”.

  12. S-12You inherit a slow Angular app. What is your performance strategy, and how do you keep it from regressing? Performance

    Say this first

    Measure first, then fix the biggest bottleneck: Core Web Vitals (LCP, INP, CLS) from real users and the Angular DevTools Profiler tell me whether the problem is loading, rendering or a busy main thread. Then I lock the gains in with bundle budgets in CI.

    Then go deeper

    • Loading: lazy routes, @defer for heavy content below the fold (never the LCP element), injectAsync() for heavy, rarely used services, NgOptimizedImage with priority on the LCP image, targeted imports checked with source-map-explorer.
    • Rendering: OnPush + signals, track on a stable id, derivations in computed() instead of template methods; for tens of thousands of rows, CDK virtual scroll or pagination — OnPush reduces how often, not how much you render.
    • Main thread: continuous calculations over ~50 ms (parsing, big filters) go into a Web Worker; transfer ArrayBuffers instead of copying huge data.
    • Regression control: budgets in angular.json that fail the build, a performance check on key pages per release, and an owner for the numbers.

    Red flag

    Optimising without measuring, or “add SSR and @defer everywhere” — including on the main content, which makes LCP worse.

  13. S-13How does Angular protect against XSS, where are the side doors, and what is your second line of defence? Security

    Say this first

    Angular treats every bound value as unsafe: interpolation escapes, [innerHTML] is sanitised, dangerous URL schemes become unsafe:. XSS in Angular apps almost always comes from bypassing that — bypassSecurityTrust* on untrusted data, nativeElement.innerHTML, eval — so the second line is a strict CSP with a nonce plus Trusted Types.

    Then go deeper

    • bypassSecurityTrust* is legitimate only for content you build and control (an embed URL from a validated id). A generic “safeHtml” pipe is a reusable XSS; search for these calls in every review.
    • CSP: Angular needs a nonce for its inline styles — ngCspNonce on the root element or the CSP_NONCE token; security.autoCsp computes hashes at build time but covers only scripts and does not work with SSR.
    • Roll out CSP with Content-Security-Policy-Report-Only first, otherwise lazy chunks or inline styles break in production.
    • CSRF with session cookies: HttpClient sends the XSRF-TOKEN cookie as X-XSRF-TOKEN on mutating, same-origin requests — but the server must set and verify it. Every frontend check is repeated on the server.

    Red flag

    “Angular sanitises everything, so we are safe” while the codebase has a bypassSecurityTrustHtml pipe applied to CMS content.

  14. S-14Design an auth interceptor with refresh tokens. What happens when six requests get a 401 at the same time, and where do you store the tokens? HTTP

    Say this first

    Exactly one refresh in flight, shared by all waiting requests (a cached Observable with shareReplay(1), cleared in finalize), then each original request is replayed with the new token. Access token in memory, refresh token in an HttpOnly, Secure, SameSite cookie — or a BFF, where the browser only has a session cookie.

    Then go deeper

    • Naive “refresh on every 401” makes N refreshes; with refresh-token rotation they invalidate each other and some servers treat reuse as an attack and kill the session.
    • Mark login/refresh with an HttpContextToken (SKIP_AUTH) so the interceptor does not intercept itself, attach the token only to your API’s URLs, and if the token already changed since the request was sent, just retry.
    • Only 401 triggers a refresh — 403 means “not allowed”. A failed refresh means logout, clearing stores and caches, and propagating logout to other tabs (BroadcastChannel).
    • At startup, a provideAppInitializer tries a refresh before the first route. With an external OIDC provider, use a certified library instead of hand-rolling PKCE and rotation.
    private refreshInFlight: Observable<string> | null = null;
    
    refresh(): Observable<string> {
      this.refreshInFlight ??= this.http
        .post<{ accessToken: string }>('/auth/refresh', null, {
          withCredentials: true,                            // HttpOnly cookie
          context: new HttpContext().set(SKIP_AUTH, true),  // not intercepted again
        })
        .pipe(
          map((r) => r.accessToken),
          tap((t) => this.token.set(t)),                    // access token: memory only
          finalize(() => (this.refreshInFlight = null)),
          shareReplay(1),                                   // every 401 waits for the same one
        );
      return this.refreshInFlight;
    }

    Red flag

    JWT in localStorage without thinking about XSS or CSP, a separate refresh for every 401, or permission checks based on decoding the JWT in the frontend.

  15. S-15What is your error-handling strategy across an Angular application? Architecture

    Say this first

    Layers: predictable errors are handled locally next to the data (a 404 or 422 shown by the store/component), cross-cutting ones centrally in an interceptor (401, 5xx), rendering errors of an area with @boundary (v22.2, developer preview), and everything else reaches a global ErrorHandler that logs to telemetry.

    Then go deeper

    • provideBrowserGlobalErrorListeners() routes window.onerror and unhandled rejections to ErrorHandler; keep the handler simple and safe — if it throws, you lose every error.
    • @boundary catches only rendering errors, not those from event handlers or async code, and a caught error is still reported to ErrorHandler.
    • The interceptor rethrows after handling (throwError(() => err)) so callers can still react; a catchError(() => of([])) without logging makes bugs invisible.
    • UX and operations: centralise and de-duplicate toasts (not five messages for one problem), translate technical errors into domain errors at the adapter, and attach context (route, version, remote) to every logged error.

    Red flag

    A try/catch or catchError in every component that swallows errors, and a toast for each one, with no central logging.

  16. S-16How do you choose between switchMap, mergeMap, concatMap and exhaustMap, and what are the pitfalls of shareReplay and backpressure? RxJS

    Say this first

    The choice is the overlap policy when a new value arrives while the previous inner request runs: switchMap cancels (typeahead), mergeMap runs in parallel (independent uploads), concatMap queues (autosave, ordered commands), exhaustMap ignores (submit, login). Wrong choices are race conditions that only show up on a slow network.

    Then go deeper

    • switchMap for writes is dangerous: a cancelled save may or may not have reached the server. Writes use concatMap or exhaustMap.
    • catchError goes inside the inner projection; placed after switchMap, the first failed request completes the whole stream and the search silently dies.
    • A plain shareReplay(1) has refCount: false: on an infinite source (websocket, interval) it never unsubscribes. Use { bufferSize: 1, refCount: true } — knowing the cache survives only if the source completed.
    • RxJS is push-based with no built-in backpressure, so you pick a policy: drop (switchMap, exhaustMap, throttleTime, auditTime, debounceTime), batch (bufferTime) or limit concurrency (mergeMap(fn, 3)). An unbounded concatMap queue on a fast source grows memory.
    readonly results$ = this.query$.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap((q) =>
        this.api.search(q).pipe(
          catchError((err) => {
            this.log.error(err);
            return of([]);        // inside: only this request fails, the stream lives on
          }),
        ),
      ),
      shareReplay({ bufferSize: 1, refCount: true }),
    );

    Red flag

    “I always use mergeMap” or nested subscribe() calls — losing cancellation, ordering and error handling.

  17. S-17What testing strategy would you set for a team: what goes into unit, component and e2e tests, and what do you mock? Testing

    Say this first

    Test behaviour, not implementation, and push each check to the cheapest level that can catch it: pure functions and stores without the DOM, dumb components through their inputs and outputs, smart pages with a fake store and HttpTestingController, and a few e2e flows (Playwright/Cypress) for what only the integration reveals. Mock at the boundaries — HTTP and ports — not your own child components.

    Then go deeper

    • Since v21 the default runner is Vitest (@angular/build:unit-test); Karma is deprecated, and old suites can be moved with the experimental refactor-jasmine-vitest schematic.
    • Zoneless tests: await fixture.whenStable() instead of fakeAsync, Vitest fake timers for debounce, TestBed.tick() to flush effects; inputs as signals via inputBinding.
    • With ports & adapters, run one contract test suite against every adapter; CDK harnesses keep component tests independent of the DOM structure.
    • In CI: nx affected plus cache for speed, no cache for non-deterministic e2e against a real backend, and coverage as a signal, not a target.

    Red flag

    “100% coverage with everything mocked” — tests that check private methods were called and break on every refactor while missing real bugs.

  18. S-18What i18n strategy would you choose: Angular’s build-time i18n or a runtime translation library? i18n

    Say this first

    Built-in @angular/localize by default: texts marked with i18n / $localize, extracted with ng extract-i18n, and one optimised build per locale with no key lookup at runtime. A runtime library only when switching language without a reload is a product requirement, or translations come from a CMS without a deploy.

    Then go deeper

    • Changing the language with built-in i18n means a reload, even with loadTranslations() at runtime, because template texts are evaluated when components are created.
    • Plurals need ICU, not n === 1 ? … : …: Romanian has one, few and other (“20 de fișiere”). Never concatenate sentences — word order differs between languages.
    • Custom ids (@@cart.title) so translations survive source edits; dates, numbers and currency through pipes with LOCALE_ID and registerLocaleData.
    • It fits SSR/prerender (one variant per locale, chosen by URL), and with hydration you add withI18nSupport(). Start on day one: adding i18n later means hunting every hard-coded string.

    Red flag

    Hard-coded formats like 'dd/MM/yyyy' or toFixed(2) + ' lei', string concatenation for sentences, and no custom message ids.

  19. S-19Your company is building its own design system in Angular. How do you make accessibility a property of the system rather than of each screen? Accessibility

    Say this first

    Build the interactive patterns on Angular Aria (@angular/aria, since v21): headless directives such as Tabs, Menu, Combobox, Listbox, Tree or Grid that give correct ARIA roles, keyboard navigation and focus management with zero styles, while the team owns the CSS. Then every product team gets accessibility by default instead of re-implementing tabs and dropdowns by hand.

    Then go deeper

    • Choice of layer: Aria for a custom design system, CDK for low-level primitives (overlay, drag & drop, a11y utilities), Material when you want a ready-made UI fast.
    • Style by the ARIA state the directives set ([aria-selected='true']), so the visual state and the accessible state cannot drift apart.
    • Trade-offs elsewhere: virtual scroll removes off-screen rows from the DOM (Ctrl+F and screen readers do not see them); prefer pagination where full accessibility matters.
    • Process: keyboard and screen-reader checks in the definition of done, automated checks in CI, and a11y review as part of the design system’s contract.

    Red flag

    “We add aria-* attributes at the end, before release” — accessibility treated as a final checklist instead of a property of the components.

  20. S-20You lead the upgrade of a large app from Angular 21 to 22. Which breaking changes worry you, and how do you roll it out? Architecture

    Say this first

    One major at a time with ng update @angular/core@22 @angular/cli@22, letting the migrations run — e.g. it adds ChangeDetectionStrategy.Eager where components had no strategy, so behaviour stays identical despite OnPush becoming the default. The real risk is in the runtime changes migrations cannot fully see: the fetch backend, router param inheritance and template semantics.

    Then go deeper

    • HTTP: fetch is the default backend and withFetch() is deprecated; upload progress needs withXhr(), and reportProgress becomes reportUploadProgress / reportDownloadProgress.
    • Router and templates: paramsInheritanceStrategy: 'always' (child routes inherit params and data from all parents), ?. returns undefined like in TypeScript, a stricter compiler (inputs bound twice, data-* no longer binds inputs), CanMatchFn needs currentSnapshot.
    • Platform and removals: TypeScript 6 and no Node 20; ComponentFactoryResolver, ComponentFactory and provideRoutes are gone; for SSR, configure allowedHosts and drop withIncrementalHydration().
    • Rollout: an upgrade branch kept green in CI, libraries and Node/TS bumped first, e2e on critical flows, then a follow-up plan to remove Eager feature by feature instead of treating it as permanent.

    Red flag

    Editing package.json versions by hand and skipping ng update, or jumping several majors in one step without reading the changelog.

How to answer well

What convinces
  • One clear sentence first, then details only if asked.
  • A concrete example from a project: “we had X, we chose Y because Z”.
  • Naming the trade-off and the case where you would choose differently.
  • “I don’t know, but this is how I would find out” when you really don’t know.
What loses points
  • Reciting definitions without saying when you would use them.
  • Old answers presented as current (NgModules everywhere, Zone.js as a given).
  • “It depends” without saying on what.
  • Inventing an API to avoid silence.

Ch. 01 · 0 of 4 complete

Finished this lesson?

Mark it and it shows up on your map: in the contents and on the home page.

Marks itself as done once you have read the whole lesson.

  1. What's new in Angular 21 & 22
  2. Your first app, step by step
  3. Quick decisions
  4. Angular interview questions
See your whole map

All 75 pages · type to filter

  • 01What's new in Angular 21 & 22All the important changes in v21 and v22, briefly, with a link to the page that explains each one.Start
  • 01Your first app, step by stepA reading list built from an empty component to a finished app — signals, @for, inputs and outputs, a service and a form, one step at a time.Start
  • 01Quick decisions“Where do I put this?”, “Signal or Observable?”, “Shared or feature?” — short answers to everyday questions.Start
  • 01Angular interview questions60 real interview questions for junior, mid and senior, with the answer to give, the follow-up and the red flags. Plus a mock interview.Start
  • 02Standalone & bootstrapHow an app starts without NgModule: bootstrapApplication, app.config.ts, imports in the component.Modern fundamentals
  • 02Data binding & eventsInterpolation, property and attribute bindings, events with $event and key filters, and how [( )] is built — what goes into the DOM and what comes back.Modern fundamentals
  • 02Control flow: @if, @for, @switchThe block syntax that replaces *ngIf/*ngFor/ngSwitch, with track, @empty and exhaustive checks.Modern fundamentals
  • 02@let and the new template expressionsLocal variables with @let, arrow functions, spread, regex, instanceof, comments inside tags.Modern fundamentals
  • 02@defer — lazy loading in templatesLoad heavy components only when needed: triggers, prefetch, placeholder, loading, error.Modern fundamentals
  • 02Class & style bindingsWhy [class.x] and [style.x] replaced ngClass and ngStyle (with automatic migration from v21).Modern fundamentals
  • 02Component styles & encapsulation:host, the three encapsulation modes, theming with CSS variables, and why ::ng-deep is a last resort.Modern fundamentals
  • 02Pipes: when and howPure vs impure pipes, custom pipes, and how you choose between a pipe, computed() and a method.Modern fundamentals
  • 03signal() and computed()The reactive primitives: writable state, derived state, equality, untracked and immutability rules.Signals & reactivity
  • 03effect() — when YES, when NOSide effects synchronised with signals: localStorage, logging, imperative APIs. And their traps.Signals & reactivity
  • 03linkedSignal() — editable derived stateA signal that resets when its source changes, but that you can also set manually.Signals & reactivity
  • 03resource, rxResource, httpResourceAsync data as signals: status, value, error, reload, automatic cancellation. Stable since v22.Signals & reactivity
  • 03debounced() — delayed signalsThe “debounce” version of a signal, without RxJS. Perfect for search.Signals & reactivity
  • 03Lists: search, filter, sort, paginateA chain of computed signals that recalculates only what changed, with the filters kept in the URL so a link reopens the same view.Signals & reactivity
  • 03Observables from zeroWhat an Observable is, subscribe and unsubscribe, the operators you use every week, cold vs hot — and when a signal is enough.Signals & reactivity
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — and when RxJS remains the right choice.Signals & reactivity
  • 04input() and output()Inputs as signals (required, transform, alias) and typed outputs. Data down, events up.Components: communication
  • 04model() — two-way bindingBanana-in-a-box [( )] with a single API. When to use it, and when input + output is better.Components: communication
  • 04viewChild & contentChildQueries as signals: viewChild, viewChildren, contentChild, contentChildren, the read option.Components: communication
  • 04Lifecycle & DestroyRefWhich hooks still matter in the signals era, afterNextRender, DestroyRef and typed ngOnChanges.Components: communication
  • 04Content projectionng-content with select, multiple slots, default content and the trap of conditional projection.Components: communication
  • 04ng-template & ngTemplateOutletTemplates as values: render props in Angular, typed context and customising components.Components: communication
  • 05Smart vs DumbContainer vs presentational: who holds the state, who only displays, and how to split them properly.Component design
  • 05Generic components <T>A correctly typed DataTable<T> or Select<T>, with templates for cells.Component design
  • 05Dynamic componentsNgComponentOutlet, createComponent with inputBinding/outputBinding, and a dashboard built from config.Component design
  • 05Directives & host compositionAttribute directives, the host: {} metadata and the Directive Composition API (hostDirectives).Component design
  • 05Custom form controlsFormValueControl (Signal Forms) vs ControlValueAccessor: a star rating, in both variants.Component design
  • 05Change detectionOnPush is the default from v22, zone.js is gone from v21. What triggers rendering and how not to get burned.Component design
  • 06inject() & @Service()The new @Service() decorator from v22, inject() vs the constructor, and the injection context.Dependency Injection
  • 06Providers & the injector hierarchyRoot vs route vs component, useClass/useValue/useFactory/useExisting, and the optional/self/skipSelf modifiers.Dependency Injection
  • 06InjectionToken & provideX()Typed configuration, provideFeature() functions, makeEnvironmentProviders and initializers.Dependency Injection
  • 06injectAsync() — lazy servicesLoad a heavy service only when you need it, with prefetch on idle.Dependency Injection
  • 07Folder structurecore / shared / features, what goes into each, and what a feature looks like inside.Application architecture
  • 07Shared vs featureWhen to move a component into shared and (especially) when NOT to. A concrete checklist.Application architecture
  • 07Interfaces & modelsInline interface or separate file? DTO vs domain model, type vs interface, where to put them.Application architecture
  • 07State management with signalsSimple signal stores, facades, local vs global state, and when you need NgRx.Application architecture
  • 07Style guide & namingThe current official conventions: file names without suffixes, protected, readonly, member order.Application architecture
  • 07Barrels, aliases & boundariesindex.ts, path aliases, a feature’s public API and dependency rules that can be checked automatically.Application architecture
  • 07Errors: @boundary & ErrorHandlerThe new @boundary block from v22.2, the global ErrorHandler, HTTP errors and errors from resource.Application architecture
  • 08Modern routing & lazy loadingloadComponent, child routes, parameters as inputs and parameter inheritance (the new default in v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate and resolve as functions, with redirects via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — the basicsForms built on signals: form(), [formField], schema-based validation. Stable since v22.Forms
  • 09Signal Forms — advancedCustom and cross-field validation, async/HTTP with debounce, dynamic lists, disabled/hidden, reusable schemas.Forms
  • 09Typed Reactive FormsTyped FormGroup, nonNullable, FormArray — and how they coexist with Signal Forms.Forms
  • 10HttpClient, interceptors & the API layerHttpClient without setup (v21), Fetch by default (v22), functional interceptors and an API layer with DTO mapping.HTTP & data
  • 11Performance — a checklistWhat really matters: OnPush + signals, track, @defer, lazy routes, images, bundles and hydration.Quality & performance
  • 11Testing with VitestVitest is the default runner from v21. TestBed, components with signals, services, HTTP and harnesses.Quality & performance
  • 11DevTools: look inside your appChrome DevTools and Angular DevTools step by step: what each panel shows, with exercises that work on this site.Quality & performance
  • 11Angular AriaHeadless, accessible components that you style however you like: tabs, accordion, menu, listbox...Quality & performance
  • 12SSR, prerender & render modesWhen to render on the server, when at build time and when only in the browser: RenderMode per route, app.routes.server.ts and the real costs.SSR & hydration
  • 12Hydration: event replay & incrementalHow the browser takes over server HTML without re-rendering it: full hydration, event replay, @defer (hydrate on …) and mismatch errors.SSR & hydration
  • 12Server-compatible codewindow doesn’t exist on the server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, the HTTP cache and PendingTasks.SSR & hydration
  • 13Micro frontendsWhen it is worth splitting an app into several deployments and when it is just complexity: Native Federation, Module Federation, iframes, Web Components.Architecture at scale
  • 13Web Components with Angular ElementscreateCustomElement: an Angular component used as an HTML tag anywhere, plus communication between apps through events and BroadcastChannel.Architecture at scale
  • 13Monorepo, libraries & boundariesA workspace with several apps and libraries: ng generate library, Nx, secondary entry points, publishing and dependency rules.Architecture at scale
  • 13Hexagonal architecture (ports & adapters)A domain without Angular, ports as abstract classes and interchangeable adapters: HTTP, localStorage or in-memory, without touching the UI.Architecture at scale
  • 14Signals under the hoodThe reactive graph: producers and consumers, push-pull, why computed is lazy and glitch-free, the diamond problem and when effects run.Advanced reactivity
  • 14Advanced RxJS: higher-order operatorsswitchMap, mergeMap, concatMap and exhaustMap on a live diagram, plus retry with backoff, shareReplay and how to avoid leaks.Advanced reactivity
  • 14NgRx SignalStoreLarge-scale state management on signals: withState, withComputed, withMethods, rxMethod, entities and hooks — and when a simple store is enough.Advanced reactivity
  • 14State machines with signalsNo more impossible states: a typed machine on signals, with transitions, guards and a live state diagram.Advanced reactivity
  • 15WebSockets: real time that survives the networkA typed socket as signals: reconnect with backoff and jitter, heartbeat, an outbox while offline, and SSR safety.Real time & offline
  • 15Optimistic updates & rollbackAn instant UI that stays honest: snapshot and rollback, out-of-order responses, idempotency keys.Real time & offline
  • 15Offline-first: Service Worker & cachingThe Angular Service Worker: caching strategies, update prompts and an outbox for changes made offline.Real time & offline
  • 16Web WorkersHeavy computation without blocking the UI: a real worker in the page, message passing and when it is not worth it.Advanced UI & performance
  • 16Huge lists: virtual scroll100,000 rows without lag: CDK virtual scroll, itemSize, trackBy and the alternatives (pagination, infinite scroll).Advanced UI & performance
  • 16Modern animations & View Transitionsanimate.enter and animate.leave instead of the deprecated @angular/animations, CSS transitions and View Transitions on navigation.Advanced UI & performance
  • 16Memory leaks: finding and fixing themSubscriptions, timers and listeners that outlive their component — how to spot them in DevTools and close them.Advanced UI & performance
  • 17Security: XSS, sanitisation & CSPWhat Angular does automatically against XSS, when bypassSecurityTrust becomes dangerous, plus CSP with nonce, Trusted Types and XSRF.Security & globalisation
  • 17Authentication: tokens & refreshAn interceptor that attaches the token, refreshes once on 401 and replays the queued requests — plus where you keep the token.Security & globalisation
  • 17OAuth 2.1 & OpenID Connect with PKCEThe authorization code flow with PKCE, step by step: verifier and challenge, state and nonce, the ID token, and why a BFF is safer.Security & globalisation
  • 17Internationalisation (i18n)$localize, the i18n attribute, ICU plural/select, a build per language vs runtime translations, and locale-based formatting.Security & globalisation