The 60 Angular questions interviewers actually ask, with direct answers, TypeScript code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds, all in modern standalone-component Angular.
60 questions with answersKey Takeaways
Angular is a front-end framework built and maintained by Google for building single-page applications and large web apps. It's TypeScript-first and opinionated: routing, forms, an HTTP client, and testing tools ship in the box, so teams get a consistent structure instead of assembling one from libraries. The reactivity story has two layers you'll be asked about: RxJS Observables for streams of async events, and signals, the newer fine-grained reactivity primitive that's reshaping how change detection works. Modern Angular is standalone by default, which means components declare their own dependencies through an imports array and most new projects have no NgModules. In interviews, Angular questions probe the object model (components, directives, dependency injection) and the runtime (change detection, zones, signals), not memorized decorator names. This page collects the 60 questions that come up most, each with a direct answer and TypeScript code. If you're still building fundamentals, Angular's official documentation is the canonical path; increasingly the first front-end round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: Angular for Beginners Course [Full Front End Tutorial with TypeScript]
Video: Angular for Beginners Course [Full Front End Tutorial with TypeScript] (freeCodeCamp.org, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your Angular certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
Angular is a TypeScript-based front-end framework from Google for building single-page and large web applications, with routing, forms, an HTTP client, and testing tools built in. It's component-based: the UI is a tree of components, each pairing a template with a class.
AngularJS (version 1) is the older, separate framework based on JavaScript, scopes, and two-way binding by default. Angular (2 and up) was a full rewrite: TypeScript, a component tree, dependency injection, and a different change detection model. They share a name and little else, so treat them as different frameworks.
Key point: A one-line definition plus the AngularJS-versus-Angular distinction is exactly what this opener screens for. Don't conflate the two.
Watch a deeper explanation
Video: Angular in 100 Seconds (Fireship, YouTube)
A component is the basic building block of an Angular UI: a TypeScript class decorated with @Component that pairs a template (the HTML view) with logic and state. The decorator wires the class to its selector, template, and styles.
The three parts you name are the class (data and methods), the template (what renders, with bindings back to the class), and the metadata in the decorator (selector, imports, change detection strategy). In modern Angular the component is standalone by default and lists its own template dependencies in imports.
import { Component } from '@angular/core';
@Component({
selector: 'app-greeting',
template: `<h1>Hello, {{ name }}</h1>`,
})
export class GreetingComponent {
name = 'Asha';
}Key point: standalone is now the default (no NgModule).
Four kinds. Interpolation ({{ value }}) and property binding ([prop]="value") push data from the class to the template. Event binding ((click)="handler()") sends events from the template to the class. Two-way binding ([(ngModel)]="value") combines both directions.
Two-way binding is just property binding plus event binding under one bracket-parenthesis syntax, the so-called banana in a box. Knowing it's sugar over the other two, not a fourth magic mechanism, is the depth interviewers look for.
// interpolation
<p>{{ title }}</p>
// property binding
<img [src]="avatarUrl" />
// event binding
<button (click)="save()">Save</button>
// two-way binding (property + event combined)
<input [(ngModel)]="name" />| Binding | Syntax | Direction |
|---|---|---|
| Interpolation | {{ value }} | Class to view |
| Property | [prop]="value" | Class to view |
| Event | (event)="handler()" | View to class |
| Two-way | [(ngModel)]="value" | Both |
Key point: The follow-up is almost always 'what is two-way binding actually made of?'. Have property-plus-event ready.
A structural directive changes the DOM layout by adding or removing elements: the built-in control flow (blocks that conditionally render or repeat) and the older *ngIf and *ngFor are structural. An attribute directive changes the appearance or behavior of an existing element without adding or removing it: ngClass, ngStyle, or a custom highlight directive.
The mental cue: structural directives shape what exists in the DOM; attribute directives decorate what's already there. The asterisk on the legacy *ngIf marks a structural directive rendered via a template.
// modern control flow (structural: adds/removes DOM)
@if (isLoggedIn) {
<p>Welcome back</p>
} @else {
<p>Please sign in</p>
}
// attribute directive (changes an existing element)
<p [ngClass]="{ active: isActive }">Status</p>Key point: Naming the new @if / @for control flow instead of only *ngIf / *ngFor indicates current knowledge.
Parent to child through inputs: the child declares an input, the parent binds a value with property binding. Child to parent through outputs: the child declares an output event and emits, the parent listens with event binding.
For anything beyond a direct parent-child pair (siblings, distant components), you lift the state into a shared service instead of threading inputs and outputs through every level. That service-plus-signal-or-subject pattern is the expected next answer.
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-rating',
template: `<button (click)="rate.emit(5)">Rate</button>`,
})
export class RatingComponent {
label = input<string>(''); // parent to child
rate = output<number>(); // child to parent
}Key point: Naming the signal-based input() and output() functions instead of the older @Input and @Output decorators indicates current Angular.
Interpolation ({{ }}) is for putting a value into text content and always produces a string. Property binding ([prop]) sets an actual DOM or component property and preserves the value's type, so it's required for non-string values (booleans, objects, arrays) and for binding to component inputs.
The practical rule: text in an element's content, use interpolation; setting a property or passing a typed value, use property binding. Trying to pass a boolean through interpolation is a classic beginner bug because it becomes the string 'true'.
A pipe transforms a value for display in the template without changing the underlying data: date, currency, uppercase, and your own custom pipes. You apply one with the pipe character, for example {{ price | currency }}.
A pure pipe (the default) only re-runs when its input reference changes, which makes it cheap and cache-friendly. An impure pipe re-runs on every change detection cycle, which is flexible but can be a performance trap. Prefer pure pipes and immutable inputs; reach for impure only when you truly need per-cycle recomputation.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'truncate' }) // pure by default
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 20): string {
return value.length > limit ? value.slice(0, limit) + '...' : value;
}
}
// usage: {{ bio | truncate:40 }}Key point: The trap: making a pipe impure to react to array mutations. The better answer is keeping data immutable so a pure pipe sees a new reference.
The async pipe subscribes to an Observable or Promise in the template, renders the latest emitted value, and unsubscribes automatically when the component is destroyed. You write {{ data$ | async }} instead of subscribing in the class.
It's recommended because it removes the two most common subscription bugs: forgetting to unsubscribe (a leak) and manually managing subscription lifecycles. It also pairs cleanly with OnPush, since it marks the component to be checked when a new value arrives.
// template
@if (user$ | async; as user) {
<p>{{ user.name }}</p>
}
// no manual subscribe, no manual unsubscribe, works with OnPushKey point: The as syntax that captures the emitted value avoids piping the same Observable through async twice, a subtle bug where you accidentally create two subscriptions.
A service is a class for logic and state that doesn't belong in a component: data fetching, business rules, shared state. Keeping this out of components makes both easier to test and reuse.
Dependency injection is how Angular supplies those services. You declare what you need (usually with the inject function or a constructor parameter), and Angular's injector creates or reuses the instance and hands it over. A service marked providedIn: 'root' becomes an app-wide singleton without any manual wiring.
import { Injectable, inject } from '@angular/core';
@Injectable({ providedIn: 'root' }) // app-wide singleton
export class CartService {
private items: string[] = [];
add(item: string) { this.items.push(item); }
}
@Component({ /* ... */ })
export class CartComponent {
private cart = inject(CartService); // injected, not new'd
}Key point: Saying 'I use inject() over constructor injection in new code' is a small signal you're writing current Angular.
Watch a deeper explanation
Video: Angular Tutorial - 18 - Dependency Injection (Codevolution, YouTube)
Lifecycle hooks are methods Angular calls at defined moments in a component's life, letting you run code at the right time. You implement the matching interface and Angular invokes the method.
The everyday set: ngOnInit (setup after inputs are first available, the usual place for initial data fetches), ngOnChanges (runs when an input value changes), ngOnDestroy (cleanup: unsubscribe, clear timers), and ngAfterViewInit (the view and any ViewChild references are ready). Doing heavy work in the constructor instead of ngOnInit is a common mistake this question screens for.
import { Component, OnInit, OnDestroy } from '@angular/core';
@Component({ /* ... */ })
export class FeedComponent implements OnInit, OnDestroy {
ngOnInit() { /* fetch initial data here, not in the constructor */ }
ngOnDestroy() { /* unsubscribe, clear timers */ }
}Key point: 'Why not fetch data in the constructor?' is the follow-up. Because inputs aren't set yet, so anything depending on them is undefined until ngOnInit.
The constructor is a TypeScript feature that runs when the class is instantiated, before Angular has set the component's inputs. Its proper job is dependency injection and simple field setup, nothing that depends on inputs or the DOM.
ngOnInit runs after Angular has set the first input values, so it's where initialization that reads inputs or fetches data belongs. The distinction matters because reading an input in the constructor gives you undefined.
Key point: The one-liner the question needs: constructor for DI, ngOnInit for input-dependent setup. State it and you've answered the follow-up too.
Modern Angular uses built-in control flow blocks: @if / @else for conditionals, @for for lists (with a required track expression), and @switch for multiple branches. They're part of the template syntax, not directives you import.
The older equivalents are the *ngIf and *ngFor structural directives, still valid and common in existing codebases. The new blocks are faster, need no imports, and make track mandatory, which nudges you toward stable list identity.
@if (users.length) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
} @else {
<p>No users yet</p>
}A selector is the CSS-style matcher in the @Component metadata that tells Angular where to instantiate the component. The common form is an element selector like app-header, used in templates as a custom tag.
Selectors can also be attribute form ([appTooltip], useful for behavior applied to existing elements) or class form. Element selectors for components, attribute selectors for directives, is the convention. The app- prefix avoids clashing with real or future HTML tags.
For a quick template-driven binding, use two-way binding with ngModel: [(ngModel)]="name" keeps the property and the input in sync. You import FormsModule (or the standalone equivalent) to get ngModel.
For anything beyond trivial forms, reactive forms are the stronger default: you define the form model in the class with FormControl and FormGroup, which gives explicit, testable, strongly typed control over values and validation. Knowing both approaches and when to use each is the fuller answer.
The Angular CLI is the official command-line tool that scaffolds and manages projects: ng new creates an app, ng generate makes components, services, and other pieces with the right files and wiring, ng serve runs a dev server, and ng build produces a production bundle.
It matters because it enforces a consistent structure and handles the build toolchain (TypeScript, bundling, optimization) so you don't hand-configure it. ng generate and ng build matters.
You define a routes array mapping URL paths to components, provide it once at the application level, and place a router outlet in the template where matched components render. Clicking a routerLink updates the URL and swaps the outlet's content without a full page reload.
That client-side navigation is what makes it a single-page app: the browser never fetches a new HTML document, the router just renders a different component tree. Route parameters, guards, and lazy loading build on this base.
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users/:id', component: UserComponent },
{ path: '**', component: NotFoundComponent }, // wildcard last
];For classes: bind a single class with [class.active]="isActive", or several at once with [ngClass]="{ active: isActive, disabled: isBusy }". For styles: [style.color]="color" for one property, or [ngStyle] for an object of properties.
Prefer the single [class.x] and [style.x] forms when you're toggling one thing; they read clearly and cost less than the object-based directives.
<button
[class.primary]="isPrimary"
[class.disabled]="isBusy"
[style.opacity]="isBusy ? 0.5 : 1"
>
Submit
</button>$event is the payload of the bound event. For native DOM events it's the browser Event object (so $event.target.value reads an input's text). For a component's custom output, $event is whatever value the child emitted.
The thing to get right: on a native input, $event is a DOM event and you dig into target.value; on a custom output, $event is already the emitted value directly. Confusing the two is a frequent beginner slip.
// native event: $event is a DOM Event
<input (input)="onInput($event)" />
// custom output: $event is the emitted value
<app-rating (rate)="onRate($event)" />A template reference variable is a name you declare in the template with a hash prefix, like <input #box />, that refers to that element, component, or directive. You can then read it elsewhere in the same template, for example calling box.value in an event handler.
It's the template-only way to grab a reference without touching the class. When you need that reference in the class instead (to focus an input from code, say), you promote it to a ViewChild query. Knowing template variables stay in the template, and ViewChild bridges to the class, is the distinction interviewers check.
<input #box (keyup.enter)="search(box.value)" />
<button (click)="search(box.value)">Go</button>
<!-- #box refers to the input element, usable anywhere in this template -->Key point: The natural follow-up is 'how do you read that reference in the class?'. Answer: a ViewChild (or the signal-based viewChild) query.
Two ways. The classic hook is ngOnChanges, which Angular calls whenever any input's bound value changes, handing you a SimpleChanges object with the previous and current values so you can respond. It runs before ngOnInit on the first change.
The modern way with signal inputs is a computed or effect that reads the input signal, so derived state updates automatically without a lifecycle hook. Signal inputs are the cleaner default in new code; ngOnChanges still appears everywhere in existing apps.
import { Component, input, computed } from '@angular/core';
@Component({ /* ... */ })
export class PriceComponent {
amount = input(0);
withTax = computed(() => this.amount() * 1.2); // reacts automatically
}Key point: Contrasting ngOnChanges with a computed over a signal input shows you know both the legacy and the current reactive approach.
Angular treats all values bound into the DOM as untrusted by default and sanitizes them for the context. Interpolated text is escaped, and values bound to risky properties like innerHTML are stripped of dangerous content before rendering, so a script tag in user data won't execute.
You can mark a value as trusted with the DomSanitizer when you genuinely control it, but that's an explicit, deliberate escape hatch. The safe-by-default behavior is why raw string concatenation into the DOM, common in other setups, isn't the Angular way.
A standalone component declares its own template dependencies in an imports array on the decorator instead of belonging to an NgModule. Since it became the default, most new Angular apps have no NgModules at all: you bootstrap a root component and provide app-wide services and routes through application config.
The payoff is less boilerplate and clearer dependencies: you can see exactly what a component uses by reading its imports, and lazy loading a single component is straightforward. NgModules still exist and appear in older codebases, so knowing both is useful, but standalone is what new work looks like.
import { Component } from '@angular/core';
import { UserCardComponent } from './user-card.component';
@Component({
selector: 'app-users',
imports: [UserCardComponent], // declares its own dependencies
template: `<app-user-card />`,
})
export class UsersComponent {}Key point: This is the single biggest 'do you know modern Angular?' question. Contrast standalone with the old NgModule declarations model to show range.
Watch a deeper explanation
Video: Getting Started with Standalone Components in Angular (Angular, YouTube)
Use the model() signal input (or the older paired input plus output named with the Changed suffix). model() gives your component a value that the parent can bind two-way with the banana-in-a-box syntax, and writing to it notifies the parent.
This is how you build reusable inputs (a rating widget, a toggle) that parents can use with [(value)] just like a native input.
import { Component, model } from '@angular/core';
@Component({
selector: 'app-toggle',
template: `<button (click)="checked.set(!checked())">{{ checked() }}</button>`,
})
export class ToggleComponent {
checked = model(false); // parent binds [(checked)]="..."
}The common approach is environment files (an environment.ts and a production variant) that the build swaps based on the target, so the same code reads a different API base URL in dev and prod. You import the environment object and reference its values.
For values that must not live in the bundle (real secrets), the technical answer is that front-end config is public by nature: anything shipped to the browser is visible, so secrets belong on the server, and the front end only holds public endpoints and keys meant to be public.
Key point: the key signal is the awareness that a front-end bundle can't hide secrets. Say it out loud; it separates juniors who know from those who don't.
For candidates with working experience: reactivity, RxJS, forms, routing, and the questions that separate users from understanders.
By default Angular runs inside a zone that patches async APIs (events, timers, XHR). When any of them fires, the zone tells Angular something might have changed, so Angular walks the component tree from the root, re-evaluates each template's bindings, and updates the DOM where values differ.
That default checks every component on every async event, which is fine for most apps but wasteful for large trees. The two levers you'll be asked about are the OnPush strategy (skip subtrees whose inputs didn't change by reference) and signals (update only the views that read a changed value, enabling zoneless operation).
One default change detection cycle
OnPush prunes whole subtrees from step 3; signals shrink it further to just the views that read a changed value.
Key point: The trap is describing change detection as 'Angular re-renders everything'. It re-checks bindings and updates only what changed; say it precisely.
OnPush tells Angular it can skip checking a component and its subtree unless one of four things happens: an input changes by reference, an event fires inside the component, an async pipe or bound signal in it emits, or you mark it manually. That prunes large parts of the check on every cycle.
Use it on components fed by immutable inputs and Observables through the async pipe, which is most well-structured components. The catch that trips people up: mutating an input object in place (instead of replacing the reference) means OnPush never sees the change, so OnPush pushes you toward immutable data.
Change detection work per event: Default vs OnPush (illustrative)
Component checks triggered by one unrelated event in a 1000-component tree (log scale). OnPush prunes untouched subtrees.
Key point: The immutability catch is the follow-up nearly every time: 'what happens if you mutate an input with OnPush on?' The answer: the view goes stale.
A signal is a reactive value: you read it by calling it like a function, and any template expression or computed that reads it is tracked. When you update the signal with set or update, Angular knows exactly which views depend on it and refreshes only those.
The two derived pieces are computed (a read-only signal derived from others, recalculated lazily and cached) and effect (runs side effects when its dependencies change). Signals matter because they let Angular do fine-grained updates without walking the whole tree, which is the foundation for zoneless change detection.
import { signal, computed } from '@angular/core';
const count = signal(0);
const doubled = computed(() => count() * 2); // derived, cached
count.set(3); // set a value
count.update(n => n + 1); // update from previous
count(); // read: 4
doubled(); // 8Key point: Being able to distinguish signal, computed, and effect from memory is the bar. Note that reading a signal is a function call, not a property access.
Signals are for synchronous reactive state that the UI reads: the current value of something now, with derived values and automatic view updates. They're simpler than RxJS for straightforward component state and don't need unsubscribing.
RxJS is for streams of asynchronous events over time: HTTP responses, user input debouncing, WebSocket messages, anything where you need operators to compose, transform, or cancel event flows. The modern pattern often uses both: RxJS at the async edges, signals for the The template reads, with toSignal bridging a stream into a signal.
| Concern | Signals | RxJS Observables |
|---|---|---|
| Best for | Synchronous UI state | Async event streams over time |
| Read model | Pull (call to read now) | Push (values arrive over time) |
| Cleanup | None needed | Must unsubscribe or use async pipe |
| Operators | computed / effect | Rich operator set (map, switchMap, etc.) |
Key point: The answer the technical value is is 'both, at different layers': don't frame signals as a replacement for RxJS, frame them as the state layer over RxJS async edges.
A Promise represents one future value, is eager (it starts as soon as it's created), resolves or rejects exactly once, and can't be cancelled. An Observable represents a stream that can emit zero, one, or many values over time, is lazy (nothing runs until you subscribe), and is cancellable by unsubscribing.
That's why Angular's HTTP client returns Observables even for a single response: you get cancellation and the full operator toolkit (retry, timeout, switchMap) for free. The lazy-versus-eager distinction is the one interviewers most want to hear.
// eager, single value, not cancellable
const p = fetch('/api/user');
// lazy, multi-value capable, cancellable
const sub = this.http.get('/api/user').subscribe(user => this.user = user);
sub.unsubscribe(); // cancels the request if still in flightWatch a deeper explanation
Video: RxJS - What and Why? (Academind, YouTube)
A Subject is both an Observable and an Observer: you can subscribe to it and also push values into it with next(). That makes it a multicast event bus, often used for cross-component communication through a service.
A plain Subject emits only to subscribers present at the time of each emission. A BehaviorSubject holds a current value and gives every new subscriber the latest value immediately, which is ideal for state (a current user, a selected filter). A ReplaySubject replays a buffer of previous values to new subscribers.
import { BehaviorSubject } from 'rxjs';
// service state new subscribers can read immediately
private user$ = new BehaviorSubject<User | null>(null);
setUser(u: User) { this.user$.next(u); }
currentUser() { return this.user$.asObservable(); }Key point: 'When would you pick BehaviorSubject over Subject?' is the follow-up. Answer: when new subscribers need the current value, that is, for state.
map (transform each value), filter (drop values), tap (side effects like logging without changing the stream), and the flattening operators are the daily set. Among flatteners, switchMap cancels the previous inner Observable when a new value arrives (right for type-ahead search and route params), mergeMap runs them in parallel, and concatMap queues them in order.
Add debounceTime and distinctUntilChanged for input handling, catchError for error recovery, and takeUntil for teardown. Being able to explain why switchMap beats mergeMap for a search box (stale responses get cancelled) is the intermediate signal.
this.searchInput$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.api.search(term)), // cancels stale requests
).subscribe(results => this.results = results);Long-lived subscriptions in a component must be torn down when it's destroyed, or they keep running and holding references. The cleanest options: use the async pipe (it subscribes and unsubscribes for you), or pipe through takeUntilDestroyed() so the stream completes when the component is destroyed.
The manual fallback is unsubscribing in ngOnDestroy, often by collecting subscriptions and calling unsubscribe on them. Note that HTTP requests that complete on their own and streams you take(1) from don't leak, so the concern is ongoing streams: router events, intervals, subjects.
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export class LiveComponent {
constructor() {
interval(1000)
.pipe(takeUntilDestroyed()) // auto-completes on destroy
.subscribe(tick => this.tick = tick);
}
}Key point: Naming takeUntilDestroyed and the async pipe over a manual ngOnDestroy subscription bucket indicates current, tidy Angular.
Template-driven forms define the form in the template with ngModel and let Angular build the model implicitly. They're quick for small forms but harder to test and scale. Reactive forms define the model explicitly in the class with FormControl, FormGroup, and FormArray, giving you a synchronous, strongly typed, testable representation of the form.
Reactive forms are the default for anything non-trivial: dynamic controls, complex validation, and value streams you can subscribe to. The rule of thumb: template-driven for a login box, reactive for real forms.
import { FormBuilder, Validators } from '@angular/forms';
private fb = inject(FormBuilder);
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
age: [null, [Validators.min(18)]],
});
// this.form.value, this.form.valid, this.form.get('email')?.errors| Template-driven | Reactive | |
|---|---|---|
| Form model | Implicit, in the template | Explicit, in the class |
| Best for | Small, simple forms | Complex, dynamic, testable forms |
| Validation | Directives in the template | Validators in code |
| Testability | Harder | Straightforward |
Watch a deeper explanation
Video: Angular Forms Tutorial - 16 - Reactive Forms (Codevolution, YouTube)
A validator is a function that takes a control and returns null when valid or an error object when not. You attach it to a control alongside the built-in validators. For validation that spans fields (password matches confirmation), you write a validator at the group level.
For validation that needs a server (is this username taken?), you use an async validator that returns an Observable or Promise of the error object or null. Explaining the sync-versus-async split, and group-level for cross-field rules, is the fuller answer.
import { AbstractControl, ValidationErrors } from '@angular/forms';
function noWhitespace(control: AbstractControl): ValidationErrors | null {
const isBlank = (control.value || '').trim().length === 0;
return isBlank ? { whitespace: true } : null;
}
// usage: this.fb.control('', [noWhitespace])You provide the HTTP client once at the application level with provideHttpClient(), inject HttpClient into a service, and call get, post, put, or delete, each returning an Observable of the typed response. Components subscribe (or bind through the async pipe) rather than calling HTTP directly.
Keep HTTP in services, not components: it centralizes endpoints, makes mocking in tests trivial, and lets you compose retries and error handling in one place. Returning a typed Observable (HttpClient generics) is expected in a strong answer.
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getUser(id: string): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
}An interceptor sits in the middle of every HTTP request and response, letting you inspect or transform them in one place. The classic uses: attach an auth token to outgoing requests, log or time calls, retry on failure, and handle 401s globally.
Modern Angular uses functional interceptors registered through provideHttpClient(withInterceptors([...])). Each interceptor clones the request (requests are immutable) to add headers, then calls next to pass it along the chain. Multiple interceptors run in the order registered.
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const cloned = req.clone({
setHeaders: { Authorization: `Bearer ${getToken()}` },
});
return next(cloned);
};
// provideHttpClient(withInterceptors([authInterceptor]))A request travelling through interceptors
Interceptors run in registration order outbound and the reverse inbound, so a logging interceptor can time the whole round trip.
Key point: 'Why clone the request instead of mutating it?' is the follow-up. Answer: HttpRequest is immutable, so clone is the only way to change it.
Guards are functions the router calls to decide whether navigation should happen. CanActivate decides whether a route can be entered (usually an auth check), CanDeactivate decides whether you can leave (warn on unsaved changes), CanMatch decides whether a route configuration even applies (useful with lazy loading), and resolvers pre-fetch data before activation.
A guard returns true, false, or a UrlTree to redirect. Modern Angular uses functional guards, plain functions that can inject services, rather than the older class-based guards.
import { CanActivateFn, Router } from '@angular/router';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn() ? true : router.parseUrl('/login');
};Key point: Returning a UrlTree to redirect, rather than false plus a manual router call, is the clean pattern interviewers like: one return decides both the block and the destination.
A resolver fetches data before a route activates, so the component renders with its data already present instead of showing an empty state and loading. The router waits for the resolver's Observable to emit, then activates the route and exposes the data through the activated route.
Use one when a route is meaningless without its data and you'd rather block navigation briefly than flash a spinner. The counterpoint to mention: resolvers can make navigation feel slow, so for large or optional data, loading inside the component with a skeleton is often the better user experience.
Key point: Naming the trade-off (resolver blocks navigation vs in-component loading with a skeleton) is what turns a definition into a judgment answer.
Lazy loading defers downloading a part of the app until its route is visited, shrinking the initial bundle. In modern Angular you lazy-load a standalone component (or a set of routes) with loadComponent or loadChildren, which return a dynamic import. The router fetches that chunk on first navigation.
The benefit is a smaller, faster first load: users only download the code for the pages they actually open. Combined with CanMatch guards, you can even avoid downloading a chunk a user isn't allowed to see.
export const routes: Routes = [
{
path: 'admin',
loadComponent: () =>
import('./admin/admin.component').then(m => m.AdminComponent),
},
];Watch a deeper explanation
Video: Angular 2 Tutorial #16 - Routing (Net Ninja, YouTube)
You inject ActivatedRoute and read parameters either as a one-time snapshot (route.snapshot.paramMap) or as an Observable stream (route.paramMap). The snapshot is fine when the component is created fresh for each navigation.
The stream is safer when navigating between two instances of the same route, for example /users/1 to /users/2. Angular reuses the component instead of recreating it, so the snapshot keeps the old value while the paramMap Observable emits the new one. Subscribing (or piping into a signal with toSignal) is the bug-free choice. Angular also offers component input binding, which maps route params straight to signal inputs.
private route = inject(ActivatedRoute);
// reacts to same-route navigation (/users/1 -> /users/2)
user$ = this.route.paramMap.pipe(
switchMap(params => this.api.getUser(params.get('id')!)),
);
// snapshot would miss the change when the component is reusedKey point: The classic bug this screens for: using the snapshot on a route the router reuses, so the page shows stale data after navigating to a sibling id.
Content projection lets a component render markup that the parent passes into it, so you can build wrapper components (cards, dialogs, panels) whose inner content is supplied by the caller. You mark the insertion point in the child's template with ng-content.
For multiple slots, ng-content takes a select attribute so different pieces of projected content land in different places (a header slot, a body slot). It's Angular's answer to composition: the shell owns the structure, the caller owns the content.
@Component({
selector: 'app-card',
template: `
<div class="card">
<header><ng-content select="[card-title]" /></header>
<div class="body"><ng-content /></div>
</div>`,
})
export class CardComponent {}
// <app-card><h2 card-title>Hi</h2><p>Body</p></app-card>ViewChild queries an element or component that lives in the component's own template, giving you a reference to interact with it (focus an input, call a child method). ContentChild queries content that was projected into the component through ng-content.
The distinction is location: ViewChild for your own view, ContentChild for content handed to you by a parent. Modern Angular offers the signal-based viewChild() and contentChild() query functions, which read reactively and avoid the older timing pitfalls around when the reference becomes available.
import { Component, viewChild, ElementRef } from '@angular/core';
@Component({ template: `<input #box />` })
export class SearchComponent {
box = viewChild<ElementRef<HTMLInputElement>>('box');
focus() { this.box()?.nativeElement.focus(); }
}Angular has a tree of injectors. A service providedIn: 'root' is a single app-wide instance. Provide a service in a component's providers array instead and you get a new instance for that component and its children, which is how you scope state to a subtree.
Injection walks up the tree: Angular looks in the requesting component's injector, then its ancestors, then the root, using the first provider it finds. Understanding this explains why the same service can be a shared singleton or a per-component instance depending on where it's provided.
Key point: 'How would you give each instance of a component its own copy of a service?' is a common probe. Answer: list it in that component's providers, not root.
An impure pipe runs on every change detection cycle, not just when its input reference changes. In a large app that fires change detection frequently, an expensive impure pipe (filtering or sorting a big array) can run hundreds of times a second and stall the UI.
The alternatives: keep data immutable so a pure pipe (which only re-runs on reference change) suffices, or move the transformation into the component and expose a precomputed value or a signal. Reach for an impure pipe only when the transform genuinely must react to internal mutations you can't make immutable.
advanced rounds probe internals, architecture judgment, and production scars. Expect every answer here to draw a follow-up.
Zoneless change detection removes the dependency on Zone.js, the library that patches async APIs to know when something might have changed. Instead, Angular relies on explicit signals: change detection runs when a signal that a template reads is updated, when an event fires, or when the async pipe emits.
It matters for performance and simplicity: no monkey-patching of the whole global async surface, smaller bundles, and updates scoped to exactly what changed rather than a full-tree check per event. The migration path is adopting signals for state and providing the zoneless change detection provider; the trade-off is that code relying on the zone to notice arbitrary async mutations must move to signals.
Key point: Framing zoneless as 'signals tell Angular what changed, so it stops guessing via Zone.js' is the crisp production-ready answer. The migration cost too matters.
Four triggers. One: an input binding receives a new reference (not a mutated same-reference object). Two: an event fires from within the component or its template (a click handler, for instance). Three: an async pipe bound in its template emits, or a signal it reads changes. Four: you call markForCheck() manually, usually after updating state outside Angular's awareness.
The senior nuance is what does not trigger it: mutating an input object in place, or a parent's unrelated state changing. That's why OnPush and immutable data go together, and why a stale OnPush view is almost always a mutation-in-place bug.
Key point: Listing all four triggers and then the non-triggers is the depth this question screens for. The mutation-in-place failure is the story they want.
Hold state in services as writable signals, expose read-only signals (or computed) to components, and mutate through explicit methods on the service. Components read signals in templates and derive view state with computed, so the UI updates automatically and no component owns shared truth.
At the async edges (HTTP, WebSockets), keep RxJS and bridge into signals with toSignal, or write results back into a signal after the stream resolves. For larger apps, a signal-based store pattern (a service with private writable signals, public computed selectors, and action methods) gives Redux-like structure without the ceremony. The judgment interviewers probe is knowing when a lightweight signal service is enough versus when a formal store earns its keep.
@Injectable({ providedIn: 'root' })
export class CartStore {
private items = signal<Item[]>([]);
readonly count = computed(() => this.items().length);
readonly total = computed(() =>
this.items().reduce((sum, i) => sum + i.price, 0));
add(item: Item) { this.items.update(list => [...list, item]); }
}Use catchError to intercept an error and return a fallback Observable (a default value, an empty stream) so the pipeline recovers instead of dying. For transient failures, retry or retryWithBackoff re-subscribes; combine with a limit and a delay so you don't hammer a failing server.
The subtle point: an unhandled error terminates the Observable, so in long-lived streams (a search box wired to switchMap) you put catchError on the inner Observable, not the outer one, or a single failed request kills the whole stream. Explaining that placement is the production signal.
this.term$.pipe(
switchMap(term =>
this.api.search(term).pipe(
catchError(() => of([])), // inner catch keeps the outer stream alive
)),
).subscribe(results => this.results = results);Key point: 'Where do you put catchError so one failed request doesn't kill the search stream?' is the trap. Inner Observable, every time.
Measure first. Use the Angular DevTools profiler to see which components are being checked and how long change detection takes, and the browser performance panel for rendering and scripting cost. Common findings: change detection running too often, expensive work in templates or impure pipes, and unbounded lists.
Then fix in order of impact: adopt OnPush or signals to prune change detection, add track to list loops so DOM nodes are reused, move heavy computation out of templates into computed values, virtualize long lists, and lazy-load routes. Bundle-size wins come from lazy loading and deferrable views. The discipline is profile-then-fix, not guessing is the technical point.
Without a track expression, when the array reference changes Angular can't tell which items are the same, so it may tear down and rebuild DOM nodes for the whole list, losing element state (focus, scroll, animations) and doing needless work.
Providing a stable key (usually an id) lets Angular match existing DOM nodes to items and only add, remove, or move what actually changed. On large or frequently updated lists this is the difference between a smooth update and a visible stall. Modern @for makes track mandatory precisely because forgetting it was such a common performance bug.
@for (row of rows(); track row.id) {
<app-row [row]="row" />
}
// track by a stable id, not $index, so reordering reuses nodesA @defer block lazily loads the components, directives, and pipes inside it, and only renders them when a trigger fires: on viewport entry, on interaction, on idle, or after a timer. It ships less JavaScript up front and defers non-critical UI without hand-writing lazy-loading plumbing.
Use it for below-the-fold or heavy widgets: a comments section, a chart, a map. Blocks support placeholder, loading, and error states declaratively. It's the template-level counterpart to route lazy loading, aimed at chunks of a single page rather than whole routes.
@defer (on viewport) {
<app-heavy-chart [data]="data()" />
} @placeholder {
<div class="skeleton">Chart loading soon</div>
} @loading {
<app-spinner />
}TestBed builds a testing module that mirrors how Angular would compile and instantiate a component: you configure it with the component and any providers (often mocked services), create a fixture, and interact with the component instance and its rendered DOM. detectChanges triggers a change detection pass so bindings update.
The strong-answer details: provide test doubles for services rather than real HTTP (HttpTestingController for HTTP), query the DOM through the fixture's debug element, and assert on rendered output and emitted outputs, that is, behavior, not private internals. For pure logic (a service, a pipe), skip TestBed and test the class directly, which is faster.
TestBed.configureTestingModule({
imports: [GreetingComponent],
providers: [{ provide: UserService, useValue: mockUserService }],
});
const fixture = TestBed.createComponent(GreetingComponent);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Hello');Key point: Saying 'I test behavior through the rendered DOM and mock the boundaries, not private methods' is the maturity signal here.
A structural directive controls whether and how a template renders by injecting TemplateRef (the content to render) and ViewContainerRef (where to render it), then calling createEmbeddedView or clear based on your logic. The asterisk in usage is sugar that wraps the host element in an ng-template.
The classic example is a permission directive that renders content only if the user has a role. Knowing the TemplateRef plus ViewContainerRef mechanism, and that the asterisk is template sugar, is what separates using structural directives from understanding them.
@Directive({ selector: '[appHasRole]' })
export class HasRoleDirective {
private tpl = inject(TemplateRef<unknown>);
private vcr = inject(ViewContainerRef);
private auth = inject(AuthService);
@Input() set appHasRole(role: string) {
this.vcr.clear();
if (this.auth.hasRole(role)) this.vcr.createEmbeddedView(this.tpl);
}
}SSR renders the app to HTML on the server so the first response is meaningful content, which helps perceived load time and SEO. When the browser JavaScript loads, hydration reuses that server-rendered DOM instead of throwing it away and re-rendering, so the page becomes interactive without a flash.
The senior points: non-destructive hydration matches existing DOM to the component tree (a mismatch, often from DOM manipulation outside Angular, breaks it), and code touching browser-only APIs (window, document) must be guarded because it also runs on the server. Incremental hydration, tied to deferrable views, hydrates parts of the page on demand.
Organize by feature, not by type: each feature owns its components, services, and routes, with lazy-loaded routes so features are separate bundles. Shared UI and utilities live in clearly separated shared libraries, and a core layer holds app-wide singletons. Many teams use a monorepo tool (Nx) to enforce module boundaries and cache builds.
The judgment interviewers probe is dependency direction: features depend on shared and core, never on each other, so the graph stays acyclic and a change in one feature can't ripple across the app. Enforcing those boundaries with tooling, not convention alone, is the mature answer.
Use FormArray for repeated groups (line items, phone numbers) you add and remove at runtime, and FormGroup nesting to mirror the data shape. Build forms from configuration so a schema drives which controls and validators exist, rather than hand-writing every field. Typed forms give you compile-time safety over control names and value shapes.
For cross-field rules, put validators at the group level; for server-dependent checks, async validators. The scaling insight: derive UI state (disabled sections, conditional fields) from valueChanges streams or signals rather than scattering imperative toggles, so the form's behavior stays declarative.
form = this.fb.group({
contacts: this.fb.array([this.makeContact()]),
});
get contacts() { return this.form.get('contacts') as FormArray; }
addContact() { this.contacts.push(this.makeContact()); }
private makeContact() {
return this.fb.group({ name: ['', Validators.required], phone: [''] });
}An InjectionToken is a unique key for providing and injecting values that aren't classes: configuration objects, strings, feature flags, or an interface (which has no runtime representation to inject by). You create the token, provide a value for it, and inject it where needed.
They're how you make configuration and cross-cutting values first-class citizens of DI instead of importing globals, which keeps things testable (override the token's value in tests) and swappable per environment. Providing a token with a factory lets the value be computed from other injected dependencies.
export const API_CONFIG = new InjectionToken<ApiConfig>('api.config');
// provide
providers: [{ provide: API_CONFIG, useValue: { baseUrl: '/api' } }];
// inject
private config = inject(API_CONFIG);XSS is the primary one, and Angular's default sanitization handles most of it by escaping interpolated values and cleaning bound HTML; the risk reappears when you bypass sanitization with the DomSanitizer or inject raw HTML, so those must guard trusted input only. CSRF is handled by Angular's HTTP client reading an anti-forgery cookie and sending it as a header when configured.
The broader points: never put real secrets in the front-end bundle because it's fully visible, validate on the server regardless of client validation, and use a Content Security Policy as defense in depth. Explaining that client-side checks are UX, not security, is the answer that indicates experience.
Key point: The line 'client-side validation is for UX, the server is the security boundary' is exactly what senior interviewers are listening for.
Incrementally, using the official migration schematics rather than a big-bang rewrite. The tooling converts components, directives, and pipes to standalone, moves NgModule imports onto the components that use them, and eventually removes the modules. You run it in stages, verify, and commit, feature by feature.
The migration order that works: convert leaf components first, then the modules that only existed to declare them, then bootstrap the root with a standalone component and application config. The failure mode to name is trying to convert everything at once and drowning in a thousand-file diff; gradual, tested steps are the professional path.
I'd flag it as a maintainability and reactivity risk, not reject it outright. Public mutable properties mean any component can change shared state from anywhere, which makes bugs hard to trace and breaks OnPush and signal-based reactivity because a raw property write notifies nobody. The symptom is stale views and race conditions that are painful to reproduce.
The better shape is a service that holds state in private signals (or a BehaviorSubject), exposes read-only access, and mutates only through named methods, so every change is intentional, observable, and traceable. I'd frame it as encapsulation plus reactivity, walk through a small refactor, and let the junior see the difference in a debugging session rather than just asserting the rule.
Key point: This is a judgment-and-mentorship question wearing an Angular costume. The technical judgment depends on whether you can reason about trade-offs and coach, not just recite a rule.
Angular wins when a team wants a full framework with strong conventions rather than a library plus a hundred decisions. It ships routing, forms, HTTP, and testing as one supported stack, which is why large organizations and long-lived enterprise apps reach for it: onboarding is faster because every Angular project looks broadly the same. The trade-off is a steeper initial learning curve (TypeScript, decorators, dependency injection, RxJS) and more ceremony for a tiny app. React gives you more freedom and a larger ecosystem but leaves architecture to you; Vue sits in the middle with a gentler ramp. Knowing these trade-offs out loud is itself an interview signal: it shows you pick tools on merits, not habit.
| Framework | Model | Best at | Watch out for |
|---|---|---|---|
| Angular | Full framework, TypeScript-first | Large enterprise apps, consistent structure | Steeper learning curve, more ceremony |
| React | Library, you assemble the stack | Flexibility, huge ecosystem, hiring pool | Architecture is on you, decision overload |
| Vue | Progressive framework | Gentle ramp, approachable API | Smaller enterprise footprint than the others |
Prepare in layers, and practice out loud. Most Angular rounds move from concept questions to live coding to a design or debugging discussion, so rehearse each stage rather than only reading answers. Build one small standalone-component app end to end; nothing exposes shaky understanding of DI, routing, or forms faster than wiring them together yourself.
The typical Angular interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages, and Angular front-end rounds are a common request. These questions reflect what actually gets asked and evaluated inside the interviews we host for 5,000+ HR teams.
See how the AI Coding Interviewer works