Blog45 min read

150+ Angular Interview Questions and Answers (2026)

150+ Angular interview questions and answers for 2026

These are the Angular interview questions I’d study before a frontend loop in 2026: 150 of them, grouped by topic and ordered from basic to senior. Every question has a full answer, and the tricky ones include a note on what the interviewer is actually checking.

Angular changed more between v16 and v22 than in the five years before. Standalone components replaced NgModules as the default, signals arrived (signal(), computed(), input(), model()), templates got built-in control flow (@if, @for, @defer), zoneless change detection became the default in v21, and OnPush became the default strategy in v22. Interviewers use these topics to tell whether your knowledge is current, so each answer marks what’s new and what’s legacy. The answers follow the official Angular documentation.

What to Expect

  • 15 sections, from fundamentals and signals to routing, forms, RxJS, SSR, testing, and architecture.
  • Direct answers first, then the depth that makes an answer strong: code, comparison tables, and pitfalls.
  • Angular interview questions for experienced developers (including 10 years experience) in the architecture section, and Angular coding interview questions at the end.
  • Angular JavaScript interview questions on TypeScript, the event loop, and RxJS, with links to FrontendLead practice problems where the concept carries over.

Angular interviews also test plain JavaScript and system design. If those are rusty, pair this list with the JavaScript interview questions and frontend interview questions guides.

Jump to a section


Angular Fundamentals

Diagram: the building blocks of an Angular app, components, templates, signals, services, the router, and the CLI

Find in-depth explanations and practice here: JavaScript Interview Questions ->

Fundamentals open almost every Angular interview. They’re quick, but a vague answer about “modules and controllers” signals that your knowledge stopped at AngularJS.

1. What is Angular?

Angular is a TypeScript-based, open-source framework from Google for building web applications. Unlike a view library, it ships the whole platform: components, a template compiler, dependency injection, a router, forms, an HTTP client, server-side rendering, and the Angular CLI for building and testing. Teams pick it when they want one consistent, opinionated way to build large apps.

2. What is the difference between AngularJS and Angular?

AngularJS (1.x) and Angular (2+) are different frameworks that share a name. Angular was a full rewrite, and AngularJS reached end of support in January 2022.

Aspect AngularJS (1.x) Angular (2+)
Language JavaScript TypeScript
Building block Controllers and $scope Components
Change detection Digest cycle with watchers Compiled views, signals, optional Zone.js
Templates ng-if, ng-repeat @if, @for (older: *ngIf, *ngFor)
Compilation Runtime, in the browser Ahead-of-time (AOT) by default

If an interviewer says “Angular”, they mean 2+. Mention AngularJS only when the question is about migration.

3. Why does Angular use TypeScript?

TypeScript gives Angular static types for components, services, inputs, and templates. The compiler type-checks templates too (with strictTemplates), so a typo in a binding or a wrong input type fails the build instead of failing in production. Decorators, generics for typed forms and HttpClient, and editor tooling all depend on it. Angular is still JavaScript at runtime: TypeScript compiles away.

4. What are the main building blocks of an Angular application?

  • Components: a TypeScript class plus a template and styles; the unit of UI.
  • Templates: HTML with bindings, control flow blocks, pipes, and directives.
  • Directives and pipes: reusable behavior on elements, and value transforms in templates.
  • Services and dependency injection: shared logic and state, provided by injectors.
  • Signals and RxJS: reactive state and async streams.
  • Router: maps URLs to components, with guards, resolvers, and lazy loading.

NgModules are on the list only for older apps. Since v19, components are standalone by default.

5. What is a standalone component, and why did Angular move to it?

A standalone component declares its own dependencies in its imports array instead of belonging to an NgModule. Angular made standalone: true the default in v19, so new components don’t need the flag. It removes a layer of indirection: you can read a component file and see everything its template uses, lazy load a single component from the router, and tree-shake unused code more easily.

import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { UserAvatar } from './user-avatar';

@Component({
  selector: 'app-header',
  imports: [RouterLink, UserAvatar], // no NgModule needed
  template: `<a routerLink="/">Home</a> <app-user-avatar />`,
})
export class Header {}

6. What is an NgModule, and do you still need one?

An @NgModule groups declarations (components, directives, pipes), imports, providers, and exports into a compilation context. It was mandatory before v14. You don’t need one in new code: standalone components, bootstrapApplication(), and provider functions like provideRouter() replace it. You’ll still meet NgModules in legacy apps and some libraries, and the two interoperate: a standalone component can import an NgModule, and an NgModule can import a standalone component.

7. How does an Angular application bootstrap?

In a modern app, main.ts calls bootstrapApplication() with the root component and an application config. Angular creates the root environment injector from the providers, renders the root component into its selector in index.html, and starts change detection.

// main.ts
bootstrapApplication(App, {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor])),
  ],
});
// Legacy: platformBrowserDynamic().bootstrapModule(AppModule)

8. What kinds of data binding does Angular support?

Binding Syntax Direction
Interpolation {{ user.name }} Component to view
Property binding [disabled]="isSaving" Component to view
Attribute, class, style [attr.aria-label], [class.active], [style.width.px] Component to view
Event binding (click)="save()" View to component
Two-way binding [(value)]="name" Both ways

9. What is the difference between property binding and attribute binding?

Property binding ([value]) sets a DOM property on the element object; attribute binding ([attr.colspan]) sets an HTML attribute. Most bindings should target properties. Use attr. for attributes that have no matching property, like ARIA attributes, colspan on some elements, or SVG attributes. Interviewers ask this to see if you know the DOM distinction: changing the value property of an input doesn’t change its value attribute.

10. How does two-way binding work (the “banana in a box”)?

[(x)] is sugar for a property binding plus an event binding: [x]="v" (xChange)="v = $event". Any component that has an input x and an output xChange supports it. In modern Angular, model() creates both in one line, and you can pass a writable signal directly: [(checked)]="isChecked" where isChecked = signal(false).

11. What does the Angular CLI do?

The CLI (ng) creates projects (ng new), generates code (ng generate component), serves with hot reload (ng serve), builds production bundles (ng build), runs tests (ng test), and runs migrations (ng update). Since v17, new projects use the esbuild-based application builder with Vite for the dev server, which made builds much faster than the old webpack builder.

12. How does Angular versioning and release work?

Angular follows semantic versioning. Until v22 it shipped a major every six months; since v22 the plan is one major per year with several minors, and each major gets 12 months of active support plus 12 months of LTS. ng update migrates one major at a time, and deprecated APIs stay for at least one major before removal. Check the release schedule for current dates.


Components and Templates

Diagram: an Angular component is a class, a template, and styles, with inputs flowing down and outputs flowing up

Find in-depth explanations and practice here: HTML Interview Questions ->

Component questions check whether you can design a clean component API: what goes in as inputs, what comes out as events, and what the parent projects in.

13. What is a component in Angular?

A component is a TypeScript class decorated with @Component that owns a piece of UI. The decorator links the class to a selector (the custom tag), a template or templateUrl, optional styles, and the imports its template uses. Angular creates an instance for each element that matches the selector and keeps the DOM in sync with the class state.

14. How do you pass data from parent to child and back?

Down with inputs, up with outputs. The modern APIs are the input() and output() functions (stable since v19); the decorators @Input() and @Output() still work.

@Component({
  selector: 'app-rating',
  template: `<button (click)="rate.emit(value() + 1)">{{ value() }} stars</button>`,
})
export class Rating {
  value = input.required<number>();   // signal input
  rate = output<number>();            // replaces @Output() + EventEmitter
}

// parent template
// <app-rating [value]="score()" (rate)="score.set($event)" />

For siblings or distant components, share state through a service instead of chaining inputs through every level.

15. What is content projection?

Content projection lets a parent pass markup into a child, which renders it where it places <ng-content>. Use the select attribute for multiple slots. It’s how you build cards, dialogs, and layout components without the child knowing what it renders.

<!-- card.html -->
<header><ng-content select="[card-title]" /></header>
<ng-content />

<!-- usage -->
<app-card>
  <h2 card-title>Billing</h2>
  <p>Your plan renews on May 1.</p>
</app-card>

Pitfall: projected content is created by the parent even if the child never shows it, so @if around <ng-content> doesn’t prevent instantiation. Use an ng-template input when content should be lazy.

16. What is the difference between ng-template, ng-container, and ng-content?

Element What it does Renders a DOM node?
ng-template Defines a template fragment that renders only when something instantiates it No (until used)
ng-container Groups elements or hosts a directive without a wrapper element No
ng-content Slot where projected parent content appears No

17. What are template reference variables?

A template reference variable (#name) gives the template a handle to an element, a component, or a directive instance. <input #q /> <button (click)="search(q.value)"> reads the input’s value without any class field. On a component tag, #ref points at the component instance; with #f="ngForm" it points at the directive exported under that name.

18. How do you query child elements and components?

Use the signal queries viewChild(), viewChildren(), contentChild(), and contentChildren() (stable since v19). They return signals, so you read them like any other signal and they update when the view changes. The legacy decorators are @ViewChild and @ContentChild.

export class SearchBox {
  input = viewChild.required<ElementRef<HTMLInputElement>>('q');
  focus() { this.input().nativeElement.focus(); }
}

View queries see the component’s own template; content queries see what the parent projected in.

19. How does view encapsulation work?

By default (ViewEncapsulation.Emulated), Angular rewrites component CSS with generated attribute selectors so styles apply only to that component’s template. ShadowDom uses the browser’s real Shadow DOM, and None makes the styles global. Use :host to style the component’s own element. Avoid ::ng-deep: it’s deprecated and leaks styles; prefer CSS custom properties to theme children.

20. What are host bindings and host listeners?

They bind properties and events on the component’s own host element. The recommended way is the host object in the decorator; @HostBinding and @HostListener are the older decorator style.

@Component({
  selector: 'app-toggle',
  host: {
    role: 'switch',
    '[attr.aria-checked]': 'on()',
    '(click)': 'on.set(!on())',
  },
  template: `{{ on() ? 'On' : 'Off' }}`,
})
export class Toggle { on = signal(false); }

21. What is the difference between a component and a directive?

A component is a directive with a template. Directives add behavior to an existing element (attribute directives like a tooltip) or change structure (structural directives). Use a component when you need your own markup; use a directive when you want to enhance any element, such as autofocus, click-outside, or permission-based visibility.

22. How do you create components dynamically?

The simplest option is NgComponentOutlet in the template: <ng-container *ngComponentOutlet="widget(); inputs: widgetInputs()" />. For full control, call ViewContainerRef.createComponent(Type) and set inputs with ComponentRef.setInput(), which correctly marks the view for check. Dynamic components are common in dashboards, modals, and CMS-driven pages. Combine them with @defer or a dynamic import() to keep them out of the main bundle.

23. How do you style a component based on state?

Bind classes and styles directly: [class.active]="isActive()", [class]="{ active: isActive(), disabled: isDisabled() }", or [style.width.%]="progress()". These are preferred over NgClass and NgStyle, which the docs now describe as unnecessary for most cases. Keep visual state in signals so the template stays declarative.

24. What is the purpose of the track expression and keys in lists?

The track expression in @for tells Angular how to identify each item, so it can move, insert, or remove DOM nodes instead of re-creating the whole list. Track by a stable id (track item.id). Tracking by $index is fine for static lists, but on reorder it re-renders rows and can attach state (like input focus) to the wrong item. The legacy equivalent is trackBy on *ngFor.


Signals

Diagram: Angular signals dependency graph, a writable signal feeds computed signals, an effect, and the template

Find in-depth explanations and practice here: Observable Autorun Engine ->

Signals are the most asked modern Angular topic. Interviewers want to know how they track dependencies, when to use computed() vs effect(), and how signals fit with RxJS.

25. What is a signal in Angular?

A signal is a wrapper around a value that notifies consumers when the value changes. You read it by calling it (count()), and Angular records the read. When the signal changes, only the computeds, effects, and templates that read it are updated. Signals became stable in v17 (signal, computed) and effect became stable in v20.

const count = signal(0);
const double = computed(() => count() * 2);

count.set(3);
count.update(c => c + 1);
console.log(double()); // 8

26. What is the difference between signal(), computed(), and effect()?

API Purpose Writable? When it runs
signal() Source state Yes: set, update N/A
computed() Derived value No Lazily, when read after a dependency changed; memoized
effect() Side effects (logging, localStorage, imperative DOM APIs) N/A Asynchronously, during change detection, after dependencies change

Rule of thumb: derive with computed(), and reach for effect() only to sync with something outside Angular.

27. How does a signal track its dependencies?

When a computed, effect, or template runs, Angular sets it as the “active consumer.” Every signal read during that run registers the consumer as a dependent. On write, the signal marks dependents as dirty (push), and they recompute only when read (pull). Because tracking is dynamic, a branch that isn’t executed doesn’t create a dependency: in computed(() => show() ? name() : ''), name isn’t tracked while show() is false.

28. Why shouldn’t you set signals inside computed()?

A computed() must be a pure derivation. Writing to a signal inside it creates hidden side effects and possible cycles, and Angular throws an error if you try. If you need “derived but locally overridable” state, use linkedSignal().

29. When should you use effect(), and when is it a smell?

Use effect() for side effects that leave Angular’s reactive graph: writing to localStorage, logging analytics, calling a charting library, or syncing to a non-Angular widget. It’s a smell when it copies one signal into another (effect(() => this.b.set(this.a() * 2))); that should be computed() or linkedSignal(). Effects need an injection context (or an explicit injector), and they clean up automatically when their component is destroyed.

export class Settings {
  theme = signal<'light' | 'dark'>('light');
  constructor() {
    effect(() => localStorage.setItem('theme', this.theme()));
  }
}

30. How do you read a signal without tracking it?

Wrap the read in untracked(). Inside an effect that should react only to userId but also log the current filter, call untracked(() => this.filter()) so filter changes don’t re-run the effect. It’s also how you call service methods from an effect without accidentally tracking signals they read.

31. What are signal inputs, and how do they differ from @Input()?

input() returns a read-only signal of the input’s value. You can derive from it with computed() instead of implementing ngOnChanges, it’s fully typed (input.required<T>() has no undefined), and it supports transform and alias. The child can’t write to it; that’s what model() is for.

export class Avatar {
  size = input(32, { transform: numberAttribute });
  user = input.required<User>();
  initials = computed(() => this.user().name.slice(0, 2).toUpperCase());
}

32. What is model(), and when do you use it?

model() declares a writable input that supports two-way binding. The child calls .set() and Angular emits a matching <name>Change event, so a parent can bind [(value)]="mySignal". Use it for form-like controls (toggles, sliders, date pickers) where the child owns edits but the parent owns the state.

export class Slider {
  value = model(0);
  nudge() { this.value.update(v => v + 1); }
}
// <app-slider [(value)]="volume" />

33. What is linkedSignal()?

linkedSignal() creates a writable signal whose value resets whenever a source changes. Classic use: a selected option that should default to the first item whenever the options list changes, but that the user can still change.

options = input.required<string[]>();
selected = linkedSignal(() => this.options()[0]);
// user picks: this.selected.set('b'); new options reset it to the first item

34. What are resource() and httpResource()?

They load async data into signals. resource() takes params (a reactive function) and a loader that returns a promise; httpResource() does the same through HttpClient, so interceptors still apply. Both expose value(), status(), error(), isLoading(), and reload(), and they cancel the previous request when params change. Check their stability status on angular.dev before relying on them in a production answer, since they were introduced as experimental.

userId = input.required<string>();
user = httpResource<User>(() => `/api/users/${this.userId()}`);
// template: @if (user.isLoading()) { Loading... } @else { {{ user.value()?.name }} }

35. How do signals and RxJS work together?

Use toSignal() to read an observable as a signal in a template, and toObservable() to feed a signal into RxJS operators like debounceTime or switchMap. Both live in @angular/core/rxjs-interop. Signals fit synchronous state; RxJS fits events over time, cancellation, and complex async orchestration. A typical search box uses both.

query = signal('');
results = toSignal(
  toObservable(this.query).pipe(
    debounceTime(300),
    switchMap(q => this.api.search(q)),
  ),
  { initialValue: [] },
);

36. Are signals the same as RxJS BehaviorSubject?

No. Both hold a current value, but a signal is synchronous, always has a value, needs no subscription or unsubscribe, and is glitch-free: computeds never observe a half-updated state. A BehaviorSubject pushes every emission to subscribers, can error and complete, and composes with operators. Signals also tell Angular exactly which views to update, which is what makes zoneless change detection practical.

37. How do signals compare to signals in other frameworks?

Same idea as SolidJS, Preact Signals, and Vue refs: fine-grained reactive values with automatic dependency tracking. Angular’s version is integrated with change detection and DI (effects need an injection context) and uses a pull-based computed(). There’s also a TC39 proposal to add signals to JavaScript. React differs: it re-renders components and doesn’t use signals for state.

38. How do you update object or array signals correctly?

Replace, don’t mutate. Signals compare with Object.is by default, so items().push(x) keeps the same reference and nothing updates. Write items.update(list => [...list, x]). You can pass a custom equal function to signal() or computed() when you want structural comparison to skip unnecessary updates.


Control Flow and @defer

Diagram: Angular built-in control flow blocks, @if, @for, @switch, and @defer

Find in-depth explanations and practice here: Code Splitting and Lazy Loading ->

Built-in control flow (stable since v18) replaced *ngIf, *ngFor, and *ngSwitch in new code. Expect to read both styles in interviews, since most codebases are mid-migration.

39. What is the new control flow syntax?

It’s template syntax built into the compiler: @if, @for, and @switch blocks. You don’t import anything, it type-narrows better, and @for is faster than NgFor because tracking is required and optimized.

@if (user(); as u) {
  <p>Hi {{ u.name }}</p>
} @else {
  <button (click)="login()">Sign in</button>
}

@for (item of items(); track item.id; let i = $index) {
  <li>{{ i + 1 }}. {{ item.title }}</li>
} @empty {
  <li>No items yet</li>
}

40. How does @for differ from *ngFor?

Aspect @for *ngFor (legacy)
Tracking track is required trackBy function, optional
Empty state Built-in @empty Separate *ngIf
Import None NgFor or CommonModule
Context vars $index, $count, $first, $last, $even, $odd Same, via let i = index

Migrate with ng generate @angular/core:control-flow.

41. What is @defer?

@defer lazy loads a block of the template. Standalone components, directives, and pipes used only inside it are split into a separate chunk and loaded when a trigger fires. It’s the easiest way to cut initial bundle size for below-the-fold widgets, charts, or comment sections.

@defer (on viewport; prefetch on idle) {
  <app-comments [postId]="id()" />
} @placeholder (minimum 300ms) {
  <p>Comments</p>
} @loading (after 100ms) {
  <app-spinner />
} @error {
  <p>Couldn't load comments.</p>
}

42. What triggers can @defer use?

  • on idle (the default): when the browser is idle.
  • on viewport: when the placeholder (or a referenced element) enters the viewport.
  • on interaction and on hover: on click/keydown or mouseover/focus.
  • on immediate and on timer(2s): right away, or after a delay.
  • when condition: when an expression becomes truthy (one-way: it doesn’t revert).

Add prefetch on ... to download the chunk earlier than it renders.

43. What are the rules for code inside @defer to actually be lazy loaded?

Dependencies are deferred only if they’re standalone and not referenced anywhere outside the block in the same file, including viewChild queries. If the component is also used eagerly elsewhere in the template, it ends up in the main bundle. Check the build output’s lazy chunk list to confirm the split worked.

44. How does @defer behave with server-side rendering?

On the server, Angular renders the @placeholder (or nothing) rather than the deferred content, and triggers are ignored. With incremental hydration, you can add hydrate on triggers so the server does render the content, and the browser hydrates that block only when the trigger fires.

45. What is @let?

@let (stable since v19) declares a read-only local variable in a template: @let total = cart().items.length;. It avoids repeating long expressions and the old *ngIf="x as y" trick for aliasing. The variable is scoped to its view and recalculated on change detection.

46. How do structural directives work under the hood?

The asterisk is sugar: <li *ngFor="let x of xs"> becomes an <ng-template ngFor let-x [ngForOf]="xs">. The directive injects TemplateRef and ViewContainerRef and calls createEmbeddedView() to stamp out views. Built-in control flow blocks compile to equivalent instructions without a directive. You still write custom structural directives for things like permission checks.

@Directive({ selector: '[appIfRole]' })
export class IfRole {
  private tpl = inject(TemplateRef);
  private vcr = inject(ViewContainerRef);
  private auth = inject(AuthService);
  appIfRole = input.required<string>();

  constructor() {
    effect(() => {
      this.vcr.clear();
      if (this.auth.roles().includes(this.appIfRole())) this.vcr.createEmbeddedView(this.tpl);
    });
  }
}

Change Detection, Zoneless, and Lifecycle

Diagram: Angular change detection, a signal update or event marks views dirty and Angular refreshes only those views without Zone.js

Find in-depth explanations and practice here: Event Loop ->

Change detection is where Angular interview questions for experienced developers get hard. Know how Zone.js worked, why Angular moved away from it, and what OnPush really skips.

47. What is change detection in Angular?

Change detection is the process that syncs component state to the DOM. Angular walks the component tree, re-evaluates template bindings, and updates only DOM nodes whose values changed. The questions are when it runs (what triggers it) and which components it checks (strategy and signals).

48. What was Zone.js, and how did it trigger change detection?

Zone.js monkey-patches browser async APIs (setTimeout, promises, DOM events, XHR). When any patched task finished, Angular’s NgZone assumed state might have changed and ran change detection from the root. It made Angular “just work” with plain mutable fields, but it ran far more checks than needed, added bundle size, and complicated stack traces. Native async/await can’t be patched, so the CLI had to downlevel it.

49. What is zoneless change detection?

Zoneless mode runs change detection only when Angular gets an explicit notification: a signal read in a template changes, a template or host listener fires, markForCheck() is called (the async pipe does this), or ComponentRef.setInput() is used. It’s the default in Angular v21+; in v20 you opt in with provideZonelessChangeDetection(). Then remove zone.js from the polyfills to save bundle size. See the zoneless guide.

50. What breaks when you go zoneless?

  • Plain fields mutated in setTimeout, promise callbacks, or subscriptions don’t update the view. Use signals or call markForCheck().
  • NgZone.onStable and onMicrotaskEmpty never emit. Use afterNextRender or afterEveryRender.
  • Reactive forms setValue calls don’t schedule change detection by themselves; reflect form state through signals or markForCheck.
  • SSR stability: register async work with the PendingTasks service so the server waits before serializing.

51. What is OnPush change detection?

ChangeDetectionStrategy.OnPush tells Angular to skip a component subtree unless one of these happened: an input received a new reference (compared with Object.is), an event was handled inside the subtree, a signal it reads changed, or markForCheck() was called (including by the async pipe). Since v22, OnPush is the default, and the old Default strategy is called Eager.

52. Why doesn’t an OnPush component update when I mutate an input object?

Because the reference didn’t change. this.user.name = 'Ana' keeps the same object, so Object.is(prev, next) is true and the child is skipped. Fix it with immutable updates (this.user = { ...this.user, name: 'Ana' }) or, better, hold the state in a signal and update with user.update(u => ({ ...u, name: 'Ana' })).

What the interviewer is checking: that you think in references, not values.

53. What do markForCheck() and detectChanges() do?

Method What it does Typical use
markForCheck() Marks the view and its ancestors dirty and schedules change detection OnPush component updated from a callback
detectChanges() Runs change detection synchronously on this view and its children now Detached views, tests, third-party callbacks
detach() / reattach() Removes the view from or returns it to the checked tree Rarely; very hot widgets

In signal-based code you rarely call any of them.

54. What causes ExpressionChangedAfterItHasBeenCheckedError?

In development mode, Angular re-checks bindings after a change detection pass. If a value changed during the pass (for example, a child’s ngAfterViewInit updates a parent field already rendered), it throws this error because the DOM would be out of sync with the state. Fix the data flow: compute the value earlier, derive it with computed(), or move the update to the event that caused it. Wrapping it in setTimeout hides the bug.

55. What are the component lifecycle hooks, in order?

  1. constructor: dependency injection; no inputs yet.
  2. ngOnChanges: when decorator-based inputs change.
  3. ngOnInit: once, after the first inputs are set.
  4. ngDoCheck: every change detection run.
  5. ngAfterContentInit / ngAfterContentChecked: projected content ready / checked.
  6. ngAfterViewInit / ngAfterViewChecked: the component’s view ready / checked.
  7. ngOnDestroy: before the component is removed.

With signal inputs, computed() often replaces ngOnChanges, and DestroyRef.onDestroy() can replace ngOnDestroy.

56. What is the difference between the constructor and ngOnInit?

The constructor runs when the class is instantiated, before Angular sets inputs. Use it (or field initializers) for inject() and setting up signals and effects. ngOnInit runs after the first inputs are available, so code that needs decorator-based input values goes there. With input() signals, reading inputs in a computed() declared as a field works, because the computed is evaluated lazily.

57. What are afterNextRender and afterEveryRender?

They run a callback after Angular finishes rendering, in the browser only (never on the server). Use afterNextRender for one-time DOM work like measuring an element or initializing a third-party chart, and afterEveryRender (formerly afterRender) for work after each render. They replace many ngAfterViewInit plus isPlatformBrowser checks, and they support read/write phases to avoid layout thrashing.

58. How do you clean up subscriptions and resources?

Prefer automatic cleanup: toSignal() and the async pipe unsubscribe for you, and takeUntilDestroyed() completes a stream when the component’s DestroyRef fires. For other cleanup, call inject(DestroyRef).onDestroy(fn). The old pattern was a destroy$ subject completed in ngOnDestroy.

constructor() {
  this.socket.messages$
    .pipe(takeUntilDestroyed())
    .subscribe(msg => this.messages.update(m => [...m, msg]));
}

59. What is NgZone.runOutsideAngular, and is it still useful?

In Zone.js apps, runOutsideAngular() runs code (like a mousemove handler or animation loop) without triggering change detection on every tick, then ngZone.run() re-enters when the UI must update. It still matters for libraries that may run in zone-based apps. In a zoneless app it just runs the function, because nothing patches the async APIs anyway.

60. How does the change detection traversal decide what to check with signals?

When a signal read in a template changes, Angular marks that specific view as needing refresh and its ancestors as having a dirty descendant. During the next pass, OnPush ancestors that weren’t otherwise dirty are traversed without being re-checked, and only the view that read the signal is refreshed. This “local change detection” (v17+) is why signals plus OnPush scale well in large trees.


Dependency Injection

Diagram: Angular injector hierarchy, a root environment injector, route injectors, and element injectors resolving a service

Find in-depth explanations and practice here: Frontend System Design Questions ->

Dependency injection is Angular’s core architecture tool. Interviewers check whether you understand the injector hierarchy, because most “why do I have two instances of my service?” bugs come from it.

61. What is dependency injection in Angular?

Dependency injection (DI) means a class asks for its dependencies instead of creating them. Angular’s injectors create and cache instances and hand them out by token. It makes services shareable (one instance per scope), swappable (provide a mock in tests or a different implementation per environment), and keeps components free of construction logic.

62. What is inject(), and why is it preferred over constructor injection?

inject(Token) retrieves a dependency from the current injection context: field initializers, constructors, provider factories, and functional guards, resolvers, and interceptors. It works without constructor parameters, so it’s easier with inheritance, it’s required for functional APIs, and it reads well with signals. Constructor injection still works.

export class OrdersPage {
  private api = inject(OrdersApi);
  private route = inject(ActivatedRoute);
  orders = toSignal(this.api.list(), { initialValue: [] });
}

63. What does providedIn: ‘root’ mean?

@Injectable({ providedIn: 'root' }) registers the service in the root environment injector, so the whole app shares one instance. It’s also tree-shakable: if nothing injects it, it isn’t in the bundle. That’s the default choice for app-wide services like API clients and auth state.

64. What is the injector hierarchy?

Angular has two hierarchies. Environment injectors: the platform injector, the root injector from bootstrapApplication, and child injectors for lazy-loaded routes with providers. Element injectors: created by components and directives that declare providers or viewProviders. Resolution starts at the requesting element, walks up the element tree, then the environment injectors, and throws NullInjectorError if nothing provides the token.

65. How do you scope a service to one component?

Add it to the component’s providers. Each component instance gets its own service instance, shared by its children and destroyed with it. This is the pattern for component-level stores, such as a wizard’s state or one instance per tab.

@Component({
  selector: 'app-checkout',
  providers: [CheckoutStore], // new instance per <app-checkout>
  template: `<app-step-shipping /> <app-step-payment />`,
})
export class Checkout {}

66. What provider types exist?

Provider Example Use for
useClass { provide: Logger, useClass: ConsoleLogger } Swapping an implementation
useValue { provide: API_URL, useValue: '/api' } Config and constants
useFactory { provide: Store, useFactory: () => new Store(inject(Http)) } Construction logic
useExisting { provide: OldApi, useExisting: NewApi } Aliasing one token to another

67. What is an InjectionToken?

An InjectionToken<T> is a typed DI key for things that aren’t classes: config objects, strings, functions, or interfaces (which don’t exist at runtime). It can include a default factory.

export const API_URL = new InjectionToken<string>('API_URL', {
  providedIn: 'root',
  factory: () => '/api',
});
const url = inject(API_URL);

68. What do the resolution modifiers optional, self, skipSelf, and host do?

They change how inject() searches. { optional: true } returns null instead of throwing. self looks only in the current element injector. skipSelf starts at the parent, which is how a nested menu finds its parent menu. host stops at the host component. The decorator versions are @Optional(), @Self(), @SkipSelf(), and @Host().

69. What are multi providers?

With multi: true, several providers add to the same token and inject() returns an array. Angular uses this for NG_VALIDATORS, NG_VALUE_ACCESSOR, and the legacy HTTP_INTERCEPTORS. You can use it for plugin systems, such as registering multiple analytics sinks.

70. How do you run code at app startup?

Use provideAppInitializer(() => inject(ConfigService).load()) (v19+). Angular waits for a returned promise or observable to finish before bootstrapping, so it’s good for loading runtime config or restoring a session. It replaces the APP_INITIALIZER token with a factory. Keep the work short: it blocks the first render.

71. What is an injection context, and how do you get one outside a constructor?

An injection context is the period when inject() is allowed: class construction, provider factories, and functions Angular calls in one (guards, resolvers, interceptors). Calling inject() in a click handler throws NG0203. If you must, capture Injector earlier and use runInInjectionContext(injector, () => ...), or pass { injector } to APIs like effect() and toSignal().


RxJS in Angular

Diagram: an RxJS search pipeline in Angular, input events flow through debounceTime, distinctUntilChanged, and switchMap to an HTTP request

Find in-depth explanations and practice here: RxJS Observable ->

Signals took over component state, but HttpClient, the router, and reactive forms still expose observables. These Angular JavaScript interview questions test whether you can pick the right flattening operator and avoid leaks.

72. What is an Observable, and how is it different from a Promise?

Aspect Observable Promise
Values Zero to many over time Exactly one
Execution Lazy: starts on subscribe Eager: starts on creation
Cancellation Unsubscribe (HttpClient aborts the request) Not built in
Operators map, filter, switchMap, retry, debounce then, catch

Practice: Promise Methods

73. What is the difference between switchMap, mergeMap, concatMap, and exhaustMap?

All four map each value to an inner observable; they differ in what they do when a new value arrives while the previous inner one is still running.

Operator New value while busy Use for
switchMap Cancels the previous inner Search, route params (latest wins)
mergeMap Runs in parallel Independent requests, like uploads
concatMap Queues, runs in order Ordered saves
exhaustMap Ignores the new value Submit and login buttons (no double submit)

What the interviewer is checking: that you don’t use switchMap for writes, where cancelling a POST can leave the server in an unknown state.

74. What is the difference between a Subject, BehaviorSubject, ReplaySubject, and AsyncSubject?

A Subject is both an observable and an observer: it multicasts values to current subscribers only. BehaviorSubject requires an initial value and gives new subscribers the latest one. ReplaySubject(n) replays the last n values. AsyncSubject emits only the final value on completion. In modern Angular, a signal often replaces a BehaviorSubject used for component state.

75. What is the difference between hot and cold observables?

A cold observable creates its producer per subscriber: each subscribe() to an HttpClient call sends a new request. A hot observable shares one producer, like DOM events or a Subject. Subscribing twice to http.get() in a template (two async pipes) causes two requests, a classic bug. Share it with shareReplay({ bufferSize: 1, refCount: true }) or convert it once with toSignal().

76. How does the async pipe work?

The async pipe subscribes to an observable or promise, returns the latest value, calls markForCheck() on each emission (so it works with OnPush and zoneless), and unsubscribes when the view is destroyed. It’s the legacy-safe way to render streams without manual subscriptions. In signal-first code, toSignal() does the same job in the class.

77. How do you avoid memory leaks with subscriptions?

  • Don’t subscribe manually when async or toSignal() can do it.
  • Use takeUntilDestroyed() for long-lived streams in components.
  • Remember that HttpClient observables complete after one response, but router events, form valueChanges, intervals, and WebSockets never complete on their own.
  • Avoid nested subscribe() calls; flatten with switchMap and friends.

78. How do you implement a debounced search with RxJS?

results$ = this.searchControl.valueChanges.pipe(
  debounceTime(300),
  map(q => q.trim()),
  distinctUntilChanged(),
  filter(q => q.length > 1),
  switchMap(q => this.api.search(q).pipe(catchError(() => of([])))),
);

Keep catchError inside switchMap: an error that reaches the outer stream completes it, and search stops working until the page reloads. Practice the underlying idea: Debounce I.

79. What are combineLatest, forkJoin, withLatestFrom, and zip?

combineLatest emits the latest value of each source whenever any emits (after all have emitted once): good for filters plus data. forkJoin waits for all sources to complete and emits their last values once: the RxJS version of Promise.all for parallel HTTP calls. withLatestFrom emits only when the main stream emits, sampling others. zip pairs values by index.

80. How do you handle errors and retries in RxJS?

Use catchError to replace an error with a fallback observable, and retry({ count: 3, delay: 1000 }) to resubscribe after failures. Only retry idempotent requests (GETs), and use a growing delay (backoff) for flaky networks. Report errors to the user through state, not just the console.

81. What does shareReplay do, and what’s its pitfall?

shareReplay(1) multicasts one subscription and replays the last value to late subscribers, which makes it a simple cache for config or reference data. The pitfall: without refCount: true, the source subscription stays alive after all subscribers leave, which leaks for never-completing sources like WebSockets.

82. When should you use RxJS vs signals in modern Angular?

Use signals for state that a template reads: current user, selected tab, derived totals. Use RxJS for events over time: debouncing, cancellation, websockets, polling, retry, and coordinating multiple async sources. Bridge them at the boundary with toSignal() and toObservable(). A senior answer avoids both extremes: not “replace all RxJS” and not “keep BehaviorSubjects for everything.”

83. What is the difference between of, from, and fromEvent?

of(1, 2, 3) emits its arguments synchronously and completes. from(x) converts an array, promise, or iterable into an observable, emitting each element. fromEvent(el, 'click') wraps DOM events and never completes, so it needs cleanup. In Angular templates, prefer (click) bindings over fromEvent.


Routing, Guards, and Resolvers

Diagram: an Angular navigation pipeline, URL match, guards, resolvers, lazy loaded component, and router outlet

Find in-depth explanations and practice here: Code Splitting and Lazy Loading ->

Routing questions focus on lazy loading and on functional guards and resolvers, which replaced class-based guards in modern Angular.

84. How do you set up routing in a standalone app?

Define a Routes array, register it with provideRouter(routes) in the app config, and place <router-outlet /> in the root template. Navigate with routerLink in templates and Router.navigate() in code. The legacy way was RouterModule.forRoot(routes) in an NgModule.

export const routes: Routes = [
  { path: '', component: Home },
  { path: 'users/:id', component: UserDetail },
  { path: '**', component: NotFound },
];

85. How do you lazy load routes?

Use loadComponent for a single standalone component or loadChildren for a group of routes. The router fetches the chunk on first navigation.

{ path: 'admin', loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES) },
{ path: 'settings', loadComponent: () => import('./settings/settings').then(m => m.Settings) },

Add withPreloading(PreloadAllModules) or a custom preloading strategy to download likely routes in the background after the first render.

86. What are route guards, and which types exist?

Guards decide whether navigation can proceed. Modern guards are plain functions that can use inject(); class-based guards (implements CanActivate) are the older style you’ll see in legacy code.

Guard Runs Example
canActivate Before activating a route Require login
canActivateChild Before any child route Protect an admin section
canDeactivate Before leaving Unsaved changes prompt
canMatch Before the route is even matched Feature flags, role-based route variants

87. How do you write a functional auth guard?

export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);
  return auth.isLoggedIn()
    ? true
    : router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};
// { path: 'account', component: Account, canActivate: [authGuard] }

Return a UrlTree to redirect instead of calling navigate() inside the guard; it avoids competing navigations. Guards are UX, not security: the API must still check permissions.

88. What is the difference between canActivate and canMatch?

canMatch runs while the router is still choosing a route, before it loads lazy code. canActivate runs after the route matched, so a lazy route’s chunk may already be downloaded by then. Use canMatch to keep unauthorized users from downloading a lazy chunk at all, or to let two routes share a path (an admin and a user dashboard at /dashboard) where the first matching route wins.

89. What is a resolver, and should you use one?

A resolver (ResolveFn) fetches data before the route activates; the component reads it from route data or as an input. It guarantees data on first render, but it also delays navigation with no visible feedback, so the app feels stuck on slow APIs. Many teams prefer navigating immediately and showing a skeleton with httpResource() or @defer, keeping resolvers for small, critical data.

export const userResolver: ResolveFn<User> = route =>
  inject(UsersApi).get(route.paramMap.get('id')!);
// { path: 'users/:id', component: UserDetail, resolve: { user: userResolver } }

90. How do you read route parameters?

With withComponentInputBinding() in provideRouter, path params, query params, and resolved data bind directly to component inputs: id = input.required<string>(). Otherwise inject ActivatedRoute and read paramMap as an observable. Use the observable (or input) rather than snapshot when the same component instance can be reused for a different id, such as moving from /users/1 to /users/2.

91. What are child routes and named outlets?

Child routes render inside a parent component’s own <router-outlet />, which suits layouts such as a settings page with tabs. Named outlets (<router-outlet name="aside" />) render a second, independent route at the same time, like a side panel or chat, with URLs such as /inbox(aside:chat).

92. What is the difference between PathLocationStrategy and HashLocationStrategy?

The default PathLocationStrategy uses clean URLs via the History API (/users/1) and needs the server to return index.html for unknown paths (or SSR). HashLocationStrategy (/#/users/1) works on any static host because the hash never reaches the server, but it’s worse for SEO and SSR. Choose path URLs unless hosting forbids rewrites.

93. How do you show a loading indicator during navigation?

Listen to router events: set loading on NavigationStart and clear it on NavigationEnd, NavigationCancel, or NavigationError. Forgetting the cancel and error cases leaves the spinner on forever when a guard rejects. You can also enable view transitions with withViewTransitions() for smoother page changes.


Forms

Diagram: Angular forms options compared, template-driven forms, reactive forms, and signal forms

Find in-depth explanations and practice here: HTML Interview Questions ->

Forms are where Angular shines in enterprise apps, so expect detailed questions: typed reactive forms, custom validators, and building your own form control.

94. What is the difference between template-driven and reactive forms?

Aspect Template-driven Reactive
Model lives in The template (ngModel) The class (FormGroup, FormControl)
Typing Weak Strongly typed since v14
Validation Directives on inputs Functions in the class
Dynamic fields, testing Harder Easier (FormArray, test without DOM)
Best for Small, simple forms Complex, dynamic, or validated forms

95. What are typed reactive forms?

Since v14, FormControl<T>, FormGroup, and FormArray infer their value types, so form.value.email is typed and typos fail at compile time. Controls are nullable by default because reset() sets them to null; use nonNullable: true or NonNullableFormBuilder to reset to the initial value instead. form.value is partial (disabled controls are omitted); getRawValue() includes everything.

private fb = inject(NonNullableFormBuilder);
form = this.fb.group({
  email: ['', [Validators.required, Validators.email]],
  age: [18, Validators.min(13)],
});
// form.getRawValue(): { email: string; age: number }

96. How do you write a custom validator?

A validator is a function that takes a control and returns null when valid or an error object when invalid.

export const noSpaces: ValidatorFn = control =>
  /\s/.test(control.value ?? '') ? { noSpaces: true } : null;

// cross-field validator on the group
export const passwordsMatch: ValidatorFn = group =>
  group.get('password')?.value === group.get('confirm')?.value ? null : { mismatch: true };

97. How do async validators work?

An AsyncValidatorFn returns an observable or promise of errors, such as checking whether a username is taken. Angular runs it only after sync validators pass, and the control’s status is PENDING meanwhile. Set updateOn: 'blur' or debounce inside the validator to avoid a request per keystroke.

98. What is FormArray used for?

FormArray holds a variable-length list of controls or groups, like multiple phone numbers or invoice line items. Add with push(), remove with removeAt(), and render with @for (row of items.controls; track row). For keyed dynamic fields, FormRecord fits better.

99. What is ControlValueAccessor?

ControlValueAccessor is the interface that lets a custom component work with formControlName and ngModel. You implement writeValue, registerOnChange, registerOnTouched, and optionally setDisabledState, then provide it with NG_VALUE_ACCESSOR. It’s how design systems build star ratings, date pickers, and rich selects that plug into any form.

@Component({
  selector: 'app-stars',
  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: Stars, multi: true }],
  template: `@for (n of [1,2,3,4,5]; track n) { <button type="button" (click)="pick(n)">{{ n <= value() ? '★' : '☆' }}</button> }`,
})
export class Stars implements ControlValueAccessor {
  value = signal(0);
  private onChange = (v: number) => {};
  private onTouched = () => {};
  writeValue(v: number) { this.value.set(v ?? 0); }
  registerOnChange(fn: (v: number) => void) { this.onChange = fn; }
  registerOnTouched(fn: () => void) { this.onTouched = fn; }
  pick(n: number) { this.value.set(n); this.onChange(n); this.onTouched(); }
}

100. What are signal forms?

Signal forms are a newer forms API (introduced in v21, in @angular/forms/signals) built on signals. You keep the model in a signal(), call form(model, schema) to get a typed field tree, bind inputs with [formField], and declare validators like required() and email() in a schema function. Field state (value(), valid(), errors(), touched()) is all signals. Reactive forms remain fully supported; check the current stability label in the signal forms guide.

login = signal({ email: '', password: '' });
loginForm = form(this.login, path => {
  required(path.email);
  email(path.email);
  required(path.password);
});
// <input [formField]="loginForm.email" />

101. What is updateOn, and when do you change it?

updateOn sets when a control updates its value and runs validation: 'change' (default, every keystroke), 'blur', or 'submit'. Use 'blur' for expensive or async validators and 'submit' for forms that should only validate when sent.

102. How do you show validation errors accessibly?

Show errors after the control is touched or the form is submitted, not on first render. Link the message to the input with aria-describedby, set [attr.aria-invalid], and move focus to the first invalid field on submit. Angular adds classes like ng-invalid and ng-touched you can style. Deep dive: Accessibility interview questions.


Directives and Pipes

Diagram: Angular directives add behavior to elements while pipes transform values in templates

Find in-depth explanations and practice here: Memoize I ->

Directives and pipes are small APIs, so interviewers often ask you to write one on the spot.

103. What are the types of directives?

Components (directives with a template), attribute directives (change the look or behavior of an element, like NgClass or a custom tooltip), and structural directives (add or remove DOM by stamping out templates, like *ngIf or a custom *appIfRole). Built-in control flow blocks now cover the most common structural cases.

104. How do you write a custom attribute directive?

@Directive({
  selector: '[appAutofocus]',
})
export class Autofocus {
  private el = inject<ElementRef<HTMLElement>>(ElementRef);
  constructor() {
    afterNextRender(() => this.el.nativeElement.focus());
  }
}
// <input appAutofocus />

Using afterNextRender keeps it safe for SSR, where there is no DOM to focus.

105. What are host directives?

hostDirectives composes directives onto a component or directive from inside its decorator, optionally exposing some of their inputs and outputs. A Button can apply FocusRing and Tooltip without every consumer adding attributes. It’s composition instead of inheritance, and it’s a common senior design-system question.

@Component({
  selector: 'app-button',
  hostDirectives: [{ directive: Tooltip, inputs: ['appTooltip: tip'] }],
  template: `<ng-content />`,
})
export class Button {}

106. What is a pipe?

A pipe transforms a value in a template: {{ price | currency:'EUR' }}, {{ createdAt | date:'short' }}. Built-ins include date, currency, number, percent, json, slice, keyvalue, titlecase, and async. Pipes keep formatting out of component classes and are cheap when pure.

107. What is the difference between pure and impure pipes?

A pure pipe (the default) re-runs only when its input reference or arguments change, so Angular effectively memoizes it. An impure pipe (pure: false) runs on every change detection pass, which is how async checks for new values. Impure pipes that filter or sort big arrays are a common performance bug. Prefer a computed() in the component for derived lists.

108. How do you write a custom pipe?

@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
  transform(value: string, max = 20): string {
    return value.length > max ? value.slice(0, max - 1) + '…' : value;
  }
}
// {{ title | truncate:30 }}

Import it in the component’s imports. Keep pipes pure and side-effect free.

109. Should you call methods in templates?

Calling a plain method like {{ total() }} that does real work runs it on every change detection pass of that view. With a signal or computed(), the call is cheap because the value is memoized. So “never call functions in templates” is outdated: calling signals is the intended pattern; calling expensive non-signal methods is the problem. Use a pure pipe or computed() for heavy work.

110. What is the difference between NgClass/NgStyle and class/style bindings?

Native bindings ([class.active], [class]="{...}", [style.color]) are built into the compiler, need no imports, and are now recommended. NgClass and NgStyle are directives from CommonModule that still work and appear in older code. Mention both and say you’d use the native bindings in new code.

111. How do i18n and locale-aware pipes work?

Angular’s built-in i18n marks text with the i18n attribute and $localize, extracts it with ng extract-i18n, and builds one bundle per locale at compile time. Pipes like date and currency follow LOCALE_ID. Runtime translation libraries (such as Transloco) are the alternative when you need to switch languages without reloading.


HTTP and Interceptors

Diagram: an Angular HttpClient request passing through a chain of functional interceptors for auth, retry, and errors before reaching the server

Find in-depth explanations and practice here: CORS ->

HTTP questions test how you centralize auth, errors, and caching instead of repeating them in every service.

112. How do you set up HttpClient in a standalone app?

Add provideHttpClient() to the app providers, with feature functions such as withInterceptors([...]) and withFetch() (uses the Fetch API, recommended for SSR). Then inject(HttpClient) in services. The legacy setup was importing HttpClientModule, which is deprecated.

@Injectable({ providedIn: 'root' })
export class UsersApi {
  private http = inject(HttpClient);
  get(id: string) { return this.http.get<User>(`/api/users/${id}`); }
}

113. What is an HTTP interceptor?

An interceptor is middleware for every HttpClient request and response: add auth headers, log, retry, show a global spinner, or map errors. Modern interceptors are functions (HttpInterceptorFn) registered with withInterceptors(); class interceptors use HTTP_INTERCEPTORS and withInterceptorsFromDi(). Requests are immutable, so you clone() them to change anything.

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).token();
  return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req);
};

114. In what order do interceptors run?

Requests flow through interceptors in the order you list them, and responses flow back in reverse order, like nested middleware. Put auth before logging if you want logs to include the header; put the error handler first if it should see errors from all others.

115. How do you handle a 401 and refresh a token?

In an interceptor, catchError on 401, call the refresh endpoint, then retry the original request with the new token. Share one in-flight refresh so ten parallel 401s trigger one refresh call, not ten, and skip the interceptor for the refresh request itself to avoid loops. If refresh fails, clear the session and redirect to login.

export const refreshInterceptor: HttpInterceptorFn = (req, next) => {
  const auth = inject(AuthService);
  return next(req).pipe(
    catchError(err =>
      err.status === 401 && !req.url.includes('/auth/refresh')
        ? auth.refreshOnce().pipe(switchMap(t => next(req.clone({ setHeaders: { Authorization: `Bearer ${t}` } }))))
        : throwError(() => err),
    ),
  );
};

116. How do you pass per-request options to an interceptor?

Use HttpContext with an HttpContextToken. For example, define SKIP_SPINNER = new HttpContextToken(() => false), send { context: new HttpContext().set(SKIP_SPINNER, true) }, and read req.context.get(SKIP_SPINNER) in the interceptor. It beats custom headers, which leak to the server.

117. How does Angular protect against XSS and CSRF?

Angular treats all template values as untrusted and sanitizes them: interpolation escapes HTML, and [innerHTML], URLs, and styles are sanitized. DomSanitizer.bypassSecurityTrust* turns that off, so use it only for content you control. For CSRF, HttpClient reads an XSRF-TOKEN cookie and sends it as an X-XSRF-TOKEN header on mutating same-origin requests; the server must validate it. Deep dive: XSS vs CORS.


SSR, Hydration, and Performance

Diagram: Angular SSR and hydration, the server renders HTML, the browser shows it, then hydration attaches listeners without re-rendering

Find in-depth explanations and practice here: SSR vs CSR ->

Performance and rendering questions separate mid-level from senior candidates. Tie every answer to a metric like LCP or INP.

118. What is Angular SSR, and how do you add it?

Server-side rendering runs the app on Node.js to produce HTML for each request, improving first paint, LCP, and SEO. Add it with ng new --ssr or ng add @angular/ssr. Angular also supports prerendering (SSG) at build time, and per-route render modes (RenderMode.Server, Prerender, Client) through server route config.

119. What is hydration?

Hydration reuses the server-rendered DOM in the browser instead of destroying and re-creating it, then attaches event listeners. Enable it with provideClientHydration(). Without it, the page flickers as Angular re-renders. Hydration requires the server and client DOM to match, so direct DOM manipulation outside Angular causes mismatch errors (opt out a component with ngSkipHydration as a last resort).

120. What are incremental hydration and event replay?

Incremental hydration (withIncrementalHydration()) leaves @defer blocks as static HTML and hydrates each only when its hydrate on trigger fires, so less JavaScript runs at startup. Event replay (withEventReplay()) records clicks that happen before hydration finishes and replays them afterwards, so early clicks aren’t lost.

@defer (hydrate on viewport) {
  <app-reviews />
}

121. How do you write SSR-safe code?

  • Don’t touch window, document, or localStorage at construction. Use afterNextRender for browser-only work.
  • Inject DOCUMENT instead of the global, and check isPlatformBrowser(inject(PLATFORM_ID)) when you must branch.
  • Avoid HTTP calls being repeated on the client: HttpClient transfer cache (on by default with hydration) reuses server responses.
  • Register non-HTTP async work with PendingTasks so the server waits for it.

122. How do you find and fix a slow Angular app?

Measure first: Chrome Performance panel, Angular DevTools profiler (shows change detection cycles per component), and Lighthouse or field data for Core Web Vitals. Then fix by cause:

Symptom Likely cause Fix
Slow first load Big initial bundle Lazy routes, @defer, budgets, SSR
Laggy typing, scrolling Too much change detection or heavy template functions OnPush, signals, computed(), pure pipes
Slow long lists Thousands of DOM nodes CDK virtual scroll, track by id
Images hurt LCP Unsized or non-prioritized images NgOptimizedImage with priority

Deep dive: Core Web Vitals.

123. What is NgOptimizedImage?

NgOptimizedImage is a directive used with ngSrc instead of src. It requires width and height (or fill) to prevent layout shift, lazy loads by default, sets fetchpriority="high" for images marked priority, generates srcset with image CDN loaders, and warns in development when the LCP image isn’t prioritized.

124. What is AOT compilation, and what are build budgets?

Ahead-of-time (AOT) compilation turns templates into JavaScript instructions at build time, so the browser doesn’t ship the compiler, templates are type-checked, and errors surface early. It’s been the default since v9 (Ivy). Budgets in angular.json fail or warn the build when bundles exceed size limits, which keeps bundle growth visible in code review.

125. How do you render a list of 10,000 rows efficiently?

Don’t render 10,000 rows. Use @angular/cdk/scrolling (cdk-virtual-scroll-viewport with *cdkVirtualFor) to render only visible rows, or paginate and infinite-scroll from the server. Track by id, keep rows OnPush, and avoid per-row subscriptions. Practice the pattern: Infinite Scrolling News Feed.


Testing

Diagram: the Angular testing pyramid, unit tests with TestBed and Vitest, component tests with harnesses, and end-to-end tests

Find in-depth explanations and practice here: Frontend Testing Guide ->

Testing questions check whether you test behavior rather than implementation details, and whether you know the modern tooling.

126. What tools does Angular use for testing?

Unit and component tests run through ng test with TestBed. Since v21, new projects use Vitest as the default runner; Karma with Jasmine is the legacy setup (Karma itself is deprecated), and Jest is common in existing codebases. End-to-end tests use Playwright or Cypress; Protractor reached end of life in 2023.

127. What is TestBed?

TestBed creates a testing environment that mimics an Angular app: you configure providers and imports, then create a component with TestBed.createComponent() and interact with the returned fixture. It runs zoneless by default in modern versions, so prefer await fixture.whenStable() to manual detectChanges().

it('shows the user name', async () => {
  TestBed.configureTestingModule({
    providers: [{ provide: UsersApi, useValue: { get: () => of({ name: 'Ana' }) } }],
  });
  const fixture = TestBed.createComponent(UserCard);
  fixture.componentRef.setInput('id', '1');
  await fixture.whenStable();
  expect(fixture.nativeElement.textContent).toContain('Ana');
});

128. How do you test a service that uses HttpClient?

Provide provideHttpClient() and provideHttpClientTesting(), inject HttpTestingController, call the service, then expectOne(url) and flush() a fake response. End with verify() to assert no unexpected requests. It tests your real request URL, method, and body without a network.

129. How do you test signal-based components and inputs?

Set inputs with fixture.componentRef.setInput('name', value), which works for both signal and decorator inputs and marks the view for check. Read signals directly in assertions (component.count()). For effects, wait for fixture.whenStable() or call TestBed.tick() to flush them.

130. What are component harnesses?

Harnesses (from @angular/cdk/testing) are test APIs for components: await loader.getHarness(MatButtonHarness.with({ text: 'Save' })) then await button.click(). Tests talk to the harness instead of CSS selectors and DOM internals, so they survive markup changes. Angular Material ships harnesses for all its components, and you can write your own for a design system.

131. How do you test a functional guard or interceptor?

Run it inside an injection context: TestBed.runInInjectionContext(() => authGuard(route, state)), with mocks provided in TestBed. Assert on the returned value (true or a UrlTree). For interceptors, register them with provideHttpClient(withInterceptors([authInterceptor])) and inspect the request in HttpTestingController.


Angular Interview Questions for Experienced Developers

Diagram: Angular interview questions for experienced developers, app architecture layers, features on top of shared UI, data access, and core services

Find in-depth explanations and practice here: Frontend System Design Questions ->

These are the Angular interview questions for experienced engineers, including staff-level candidates and Angular interview questions for 10 years experience. Answer with a decision rule and a trade-off, not a definition.

132. How would you structure a large Angular application?

Organize by feature, not by file type. Each feature folder owns its routes, components, and state, and is lazy loaded. Shared code splits into UI (presentational components), data access (API services, stores), and utilities, with lint rules (for example Nx module boundaries) that stop features from importing each other’s internals. Keep a thin core for app-wide singletons like auth and logging.

Layer Contains Can import
Feature Routed pages, smart components UI, data access, utils
UI Presentational components Utils only
Data access API services, stores Utils only
Utils Pure functions, types Nothing app-specific

133. What are smart and presentational components?

Smart (container) components inject services, load data, and handle side effects. Presentational components receive data through inputs, emit outputs, and hold no business logic, which makes them reusable, easy to test, and cheap to render with OnPush. A senior answer adds that the split is a guideline: a small leaf component injecting a service is fine if it keeps the tree simpler.

134. How do you choose a state management approach?

Need Choose
Local UI state Signals in the component
Shared state for one feature A service with signals (a “signal store” service), provided at the feature route
Server data with caching httpResource(), or a query library
Large app, strict event history, many teams NgRx Store (actions, reducers, effects) or NgRx SignalStore

“I start with signals in services and only add NgRx when we need devtools time travel, strict unidirectional flow across teams, or complex effect orchestration.”

135. What does a signal-based service store look like?

@Injectable({ providedIn: 'root' })
export class CartStore {
  private readonly items = signal<CartItem[]>([]);
  readonly list = this.items.asReadonly();
  readonly total = computed(() => this.items().reduce((s, i) => s + i.price * i.qty, 0));

  add(item: CartItem) {
    this.items.update(list => [...list, item]);
  }
  remove(id: string) {
    this.items.update(list => list.filter(i => i.id !== id));
  }
}

Writes go through methods, reads are read-only signals, and derived values are computed. That’s most of what a store library gives you, without the dependency.

136. How would you migrate a legacy Angular app to modern Angular?

  1. Update one major at a time with ng update, fixing deprecations on the way.
  2. Run the official schematics: standalone migration, control flow migration, inject() migration, and signal input and query migrations.
  3. Switch to the application builder (esbuild) and functional guards and interceptors.
  4. Adopt OnPush and signals feature by feature, then go zoneless once nothing relies on Zone.js.

Ship each step separately behind tests, so regressions are easy to bisect.

137. How would you migrate from AngularJS to Angular?

Big-bang rewrites rarely ship. The incremental options are ngUpgrade (run both frameworks in one app and migrate component by component, now an older approach), or a strangler pattern: route new pages to a new Angular app while the old one serves the rest, sharing auth through cookies. Prioritize high-change pages first, and set a date to delete the old shell.

138. When would you use micro frontends with Angular?

When several teams need to deploy independently, not as a performance fix. Options include Module Federation or Native Federation (sharing Angular as a singleton), or web components built with Angular Elements. Costs: duplicated dependencies if versions drift, harder shared state, and cross-app routing. Deep dive: Micro frontends and backend for frontend.

139. How do you build a shared component library in Angular?

Generate a library (ng generate library), build it with ng-packagr, and publish it or consume it in a monorepo. Use standalone components, signal inputs, CSS custom properties for theming, hostDirectives for shared behavior, and ControlValueAccessor for form controls. Add component harnesses and accessibility tests, and use the Angular CDK (overlay, a11y, drag and drop) instead of reinventing primitives.

140. How does Angular compare to React for a new project?

Aspect Angular React
Scope Full framework: router, forms, HTTP, DI, CLI UI library; pick router, data, and forms libraries
Reactivity Signals, fine-grained updates Re-render plus reconciliation; React Compiler for memoization
Consistency High across teams Varies by stack choices
Fits best Large enterprise apps, many teams Flexible stacks, large ecosystem

Pick based on team skills and how much convention you want. Compare more frameworks in frontend frameworks.

141. How would you design a real-time dashboard in Angular?

Open one WebSocket in a root service and expose typed streams per topic. Buffer high-frequency updates (bufferTime or requestAnimationFrame) and write them into signals, so each widget re-renders only for its own data. Lazy load widgets with @defer (on viewport), virtualize tables, reconnect with backoff, and show stale-data indicators. For a full walkthrough of this kind of interview, see Design a Live Sports Scoreboard.


Angular Coding Interview Questions

Diagram: an Angular coding interview workflow, clarify requirements, model state with signals, build the template, and handle edge cases

Find in-depth explanations and practice here: JavaScript Coding Questions ->

Angular coding interview questions are usually small features built live: a component, a directive, or a service. Talk through state first, then template, then edge cases. A few rounds also ask you to implement the JavaScript primitive behind an Angular feature.

142. Build a counter component with signals.

Requirements: increment, decrement (not below zero), reset, and show whether the count is even. The key idea is one source signal and derived state with computed().

@Component({
  selector: 'app-counter',
  template: `
    <button (click)="dec()" [disabled]="count() === 0">-</button>
    <span>{{ count() }} ({{ isEven() ? 'even' : 'odd' }})</span>
    <button (click)="count.update(c => c + 1)">+</button>
    <button (click)="count.set(0)">Reset</button>
  `,
})
export class Counter {
  count = signal(0);
  isEven = computed(() => this.count() % 2 === 0);
  dec() { this.count.update(c => Math.max(0, c - 1)); }
}

143. Implement a minimal signal, computed, and effect in plain JavaScript.

This tests whether you understand dependency tracking, not just the API. Keep a global “current consumer”; reading a signal subscribes the current consumer, and writing notifies subscribers.

let current = null;

function signal(value) {
  const subs = new Set();
  const read = () => {
    if (current) subs.add(current);
    return value;
  };
  read.set = next => {
    if (Object.is(next, value)) return;
    value = next;
    [...subs].forEach(fn => fn());
  };
  return read;
}

function effect(fn) {
  const run = () => {
    const prev = current;
    current = run;
    try { fn(); } finally { current = prev; }
  };
  run();
}

function computed(fn) {
  const s = signal(undefined);
  effect(() => s.set(fn()));
  return () => s();
}

const price = signal(10);
const qty = signal(2);
const total = computed(() => price() * qty());
effect(() => console.log('total', total()));
qty.set(3);
price.set(10); // same value: no update

// Output:
// total 20
// total 30

Follow-ups: Angular’s real computed() is lazy and memoized (it only recomputes when read), dependencies are re-collected on each run, and effects are scheduled, not run synchronously. Related practice: Observable Autorun Engine.

144. Build a typeahead search component.

Requirements: debounce input, cancel stale requests, ignore duplicate queries, and show loading and empty states.

@Component({
  selector: 'app-typeahead',
  imports: [ReactiveFormsModule],
  template: `
    <input [formControl]="q" placeholder="Search users" aria-label="Search users" />
    @if (loading()) { <p>Loading...</p> }
    <ul>
      @for (u of results(); track u.id) { <li>{{ u.name }}</li> }
      @empty { @if (!loading() && q.value.length > 1) { <li>No matches</li> } }
    </ul>
  `,
})
export class Typeahead {
  private api = inject(UsersApi);
  q = new FormControl('', { nonNullable: true });
  loading = signal(false);
  results = toSignal(
    this.q.valueChanges.pipe(
      debounceTime(300),
      map(v => v.trim()),
      distinctUntilChanged(),
      tap(() => this.loading.set(true)),
      switchMap(v => (v.length < 2 ? of([]) : this.api.search(v).pipe(catchError(() => of([]))))),
      tap(() => this.loading.set(false)),
    ),
    { initialValue: [] },
  );
}

What the interviewer is checking: switchMap for cancellation, catchError inside it, and accessible labels. Related practice: Autocomplete Search List.

145. Build a todo list with add, toggle, delete, and filters.

type Filter = 'all' | 'active' | 'done';
interface Todo { id: number; title: string; done: boolean; }

@Component({
  selector: 'app-todos',
  template: `
    <input #t (keyup.enter)="add(t.value); t.value = ''" aria-label="New todo" />
    @for (f of filters; track f) {
      <button (click)="filter.set(f)" [class.active]="filter() === f">{{ f }}</button>
    }
    <ul>
      @for (todo of visible(); track todo.id) {
        <li>
          <input type="checkbox" [checked]="todo.done" (change)="toggle(todo.id)" />
          {{ todo.title }} <button (click)="remove(todo.id)">Delete</button>
        </li>
      }
    </ul>
    <p>{{ remaining() }} left</p>
  `,
})
export class Todos {
  filters: Filter[] = ['all', 'active', 'done'];
  todos = signal<Todo[]>([]);
  filter = signal<Filter>('all');
  visible = computed(() => {
    const f = this.filter();
    return this.todos().filter(t => f === 'all' || (f === 'done') === t.done);
  });
  remaining = computed(() => this.todos().filter(t => !t.done).length);
  private nextId = 1;

  add(title: string) {
    if (!title.trim()) return;
    this.todos.update(l => [...l, { id: this.nextId++, title: title.trim(), done: false }]);
  }
  toggle(id: number) {
    this.todos.update(l => l.map(t => (t.id === id ? { ...t, done: !t.done } : t)));
  }
  remove(id: number) {
    this.todos.update(l => l.filter(t => t.id !== id));
  }
}

146. Implement debounce, then use it in a directive.

Debounce delays a call until the input has been quiet for a given time. Each call clears the previous timer.

function debounce(fn, wait) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), wait);
  };
}

const log = debounce(q => console.log('search', q), 50);
log('a');
log('an');
log('ang');

// Output (after 50ms):
// search ang

In Angular, wrap it in a directive that emits a debounced output, and clear the timer on destroy so a late call doesn’t fire after the component is gone.

@Directive({ selector: 'input[appDebounce]', host: { '(input)': 'onInput($event)' } })
export class DebounceInput {
  delay = input(300);
  debounced = output<string>();
  private timer?: ReturnType<typeof setTimeout>;
  constructor() { inject(DestroyRef).onDestroy(() => clearTimeout(this.timer)); }
  onInput(e: Event) {
    clearTimeout(this.timer);
    const value = (e.target as HTMLInputElement).value;
    this.timer = setTimeout(() => this.debounced.emit(value), this.delay());
  }
}

Practice: Debounce II

147. Write a click-outside directive.

Emit when a click lands outside the host element, for closing dropdowns and menus. Listen on the document through the host object, and compare with contains().

@Directive({
  selector: '[appClickOutside]',
  host: { '(document:click)': 'onDocClick($event)' },
})
export class ClickOutside {
  private el = inject(ElementRef);
  appClickOutside = output<void>();
  onDocClick(e: MouseEvent) {
    if (!this.el.nativeElement.contains(e.target as Node)) this.appClickOutside.emit();
  }
}
// <div class="menu" (appClickOutside)="close()">...</div>

Edge case: the click that opens the menu can bubble to the document and close it immediately; open on pointerdown or check the event target.

148. Implement a typed event bus service.

Components need to publish and subscribe to app events without direct references. The plain JavaScript core is an emitter; the Angular version wraps a Subject in a root service.

class EventBus {
  #handlers = new Map();
  on(type, fn) {
    if (!this.#handlers.has(type)) this.#handlers.set(type, new Set());
    this.#handlers.get(type).add(fn);
    return () => this.#handlers.get(type).delete(fn);
  }
  emit(type, payload) {
    this.#handlers.get(type)?.forEach(fn => fn(payload));
  }
}

const bus = new EventBus();
const off = bus.on('cart:add', item => console.log('added', item));
bus.emit('cart:add', 'book');
off();
bus.emit('cart:add', 'pen');

// Output:
// added book
type AppEvent = { type: 'cart:add'; item: string } | { type: 'logout' };

@Injectable({ providedIn: 'root' })
export class Bus {
  private events = new Subject<AppEvent>();
  emit(e: AppEvent) { this.events.next(e); }
  on<T extends AppEvent['type']>(type: T) {
    return this.events.pipe(filter((e): e is Extract<AppEvent, { type: T }> => e.type === type));
  }
}

Senior note: a global bus hides data flow, so prefer explicit state services and use a bus only for truly cross-cutting events. Practice: Event Emitter.

149. Build a sortable, paginated table.

Keep the raw rows, sort key, direction, and page in signals, and derive the visible page with computed(). Sorting copies the array, since sort() mutates.

export class UsersTable {
  rows = input.required<User[]>();
  sortKey = signal<keyof User>('name');
  asc = signal(true);
  page = signal(0);
  pageSize = 10;

  sorted = computed(() => {
    const key = this.sortKey();
    const dir = this.asc() ? 1 : -1;
    return [...this.rows()].sort((a, b) => (a[key] > b[key] ? dir : a[key] < b[key] ? -dir : 0));
  });
  visible = computed(() => this.sorted().slice(this.page() * this.pageSize, (this.page() + 1) * this.pageSize));
  pages = computed(() => Math.ceil(this.rows().length / this.pageSize));

  sortBy(key: keyof User) {
    this.asc.set(this.sortKey() === key ? !this.asc() : true);
    this.sortKey.set(key);
    this.page.set(0);
  }
}

Mention aria-sort on the active header and resetting to page 0 when the sort changes.

150. Debug this: the view doesn’t update after data loads.

@Component({
  selector: 'app-profile',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `{{ user?.name }}`,
})
export class Profile {
  user?: User;
  constructor() {
    inject(UsersApi).me().subscribe(u => (this.user = u)); // view stays empty
  }
}

The component is OnPush (and the app may be zoneless), and a plain field assigned in a subscription callback doesn’t notify Angular. The subscription also never gets cleaned up if me() doesn’t complete. Fix it by making the state a signal: user = toSignal(inject(UsersApi).me()) and {{ user()?.name }}. That notifies change detection and unsubscribes on destroy.


How to Use This List

Don’t read 150 questions the night before. Pick the sections that match the role, answer each question out loud before reading the answer, and build at least one small feature with signals, a functional guard, and an interceptor. Interviewers can tell when you’ve only read about @defer and zoneless mode.

Level Focus on Then practice
Junior Fundamentals, components and templates, control flow, forms A todo list, a validated signup form
Mid-level Signals, RxJS, routing, HTTP and interceptors, testing A typeahead, an auth guard plus token interceptor
Senior / 10 years experience Change detection and zoneless, DI, SSR and performance, architecture A legacy-to-modern migration plan, a real-time dashboard design

Angular interview questions for experienced developers lean on trade-offs: when to use OnPush, signals vs RxJS, resolvers vs skeletons, NgRx vs a signal service. Practice explaining your choice in two sentences, then the cost of the alternative.

Where to go next on FrontendLead

The Angular interview questions and answers above reflect Angular v22. When a newer version ships, check the official docs for API status changes, since several APIs here moved from developer preview to stable in the last few releases.

Related reading: 120+ TypeScript interview questions and answers