Angular Interview Questions and Answers
96 hand-picked Angular interview questions with
detailed answers. Open the interactive version above to search, filter
by difficulty, run code, bookmark questions and track your progress.
What is Angular and how is it different from AngularJS?
Angular (v2+) is a TypeScript-based, component-driven SPA framework by Google. It ships a full platform: components, dependency injection, routing, forms, HTTP, and RxJS.
AngularJS (v1.x) was JavaScript-based and used controllers, scopes, and two-way digest-cycle binding. Angular is a complete rewrite: component architecture, a hierarchical injector, AOT compilation, and far better performance.
Lifecycle hooks — order and what each is for.
Order: constructor → ngOnChanges → ngOnInit → ngDoCheck → ngAfterContentInit → ngAfterContentChecked → ngAfterViewInit → ngAfterViewChecked → ngOnDestroy.
- ngOnChanges — runs when an
@Input changes (receives SimpleChanges). - ngOnInit — one-time init; fetch data here, not in the constructor.
- ngAfterViewInit — view and
@ViewChild refs are ready. - ngOnDestroy — cleanup: unsubscribe, clear timers.
What are the types of data binding?
- Interpolation —
{{ value }} (component → view). - Property binding —
[src]="url" (component → view). - Event binding —
(click)="fn()" (view → component). - Two-way binding —
[(ngModel)]="name" (both directions; the "banana in a box").
What is the Change Detection (CD) Tree in Angular?
The Change Detection (CD) Tree is Angular's internal tree of component views. Every component becomes a node in this tree. During change detection, Angular starts at the root component and traverses the tree, checking each component for data changes and updating the DOM when necessary.
Components using the OnPush strategy can skip change detection unless they are explicitly marked for checking or one of their inputs changes, making the CD tree traversal more efficient.
AppComponent
├── HeaderComponent
├── DashboardComponent
│ ├── UserCardComponent
│ └── ChartComponent
└── FooterComponent
What are the types of directives?
- Component — a directive with a template (the most common).
- Structural — change DOM layout:
*ngIf, *ngFor, *ngSwitch (v17+: @if, @for). - Attribute — change appearance/behaviour:
ngClass, ngStyle, or your own.
How do components communicate with each other?
- Parent → child:
@Input(). - Child → parent:
@Output() with an EventEmitter. - Unrelated components: a shared service with a
BehaviorSubject (or a signal / NgRx store). - Parent accessing child:
@ViewChild.
@Output() saved = new EventEmitter<Item>();
onSave() { this.saved.emit(this.item); }
Explain Dependency Injection (DI) and providedIn: 'root' in Angular.
Dependency Injection (DI) is a design pattern where Angular automatically creates and provides the objects (dependencies) a class needs instead of the class creating them itself. Angular uses a hierarchical injector to manage these dependencies.
@Injectable({ providedIn: 'root' }) registers a service in the application's root injector, making it a singleton shared across the entire application. It is also tree-shakable, so unused services are removed from production builds.
@Injectable({
providedIn: 'root'
})
export class UserService {
users = ['John', 'Alice'];
}
@Component({...})
export class HomeComponent {
constructor(private userService: UserService) {}
}
Template-driven vs Reactive forms — which and why?
Template-driven — logic lives in the template via ngModel; quick, good for simple forms.
Reactive — the form model is defined in the TS class (FormGroup, FormControl, FormBuilder); explicit, testable, and better for complex or dynamic validation.
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
});
Explain routing: lazy loading, guards, and resolvers.
- Lazy loading — load a feature only when visited via
loadChildren / loadComponent. Smaller initial bundle, faster first paint. - Guards —
CanActivate (auth), CanDeactivate (unsaved-changes prompt), CanMatch. - Resolver — pre-fetch data before the route activates so the component opens with data ready.
{ path: 'admin', loadComponent: () =>
import('./admin.component').then(m => m.AdminComponent) }
What are HTTP interceptors used for?
HTTP Interceptors are middleware that intercept every outgoing HTTP request and incoming HTTP response in an Angular application. They allow you to add, modify, or handle requests and responses globally without changing individual service methods. Common uses include attaching JWT tokens, adding custom headers, logging, displaying global loaders, handling errors, retrying failed requests, and refreshing expired access tokens.
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem('token');
const clonedReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(clonedReq);
}
}
Explain content projection (ng-content) and ViewChild/ContentChild.
ng-content projects markup that a parent passes into a child (like slots) — key for reusable UI such as cards and modals.
@ViewChild references an element/component in this template; @ContentChild references projected content. They're ready in ngAfterViewInit and ngAfterContentInit respectively.
How does change detection work? Default vs OnPush.
zone.js patches async APIs and tells Angular to re-check the component tree after events. The Default strategy checks every component each cycle.
OnPush only re-checks a component when: an @Input reference changes, an event fires inside it, or an async pipe emits. Combined with immutable data it drastically cuts re-renders. (Signals push toward zone-less change detection.)
@Component({ changeDetection: ChangeDetectionStrategy.OnPush })
List concrete performance optimisations in Angular.
ChangeDetectionStrategy.OnPush + immutable data.trackBy in *ngFor (or track in @for) to reuse DOM nodes.- Lazy load feature routes; add a preloading strategy for the rest.
- Pure pipes instead of method calls in templates.
- Virtual scrolling (
cdk-virtual-scroll) for long lists. - async pipe to avoid manual-subscription leaks.
Pure vs impure pipes — and what is a pipe?
A pipe transforms a value in the template ({{ date | date:'short' }}).
Pure (default) — recomputes only when the input reference changes; cheap. Impure — runs every change-detection cycle (e.g. async, or a filter over a mutating array); powerful but costly, use sparingly.
@Pipe({ name: 'myFilter', pure: false })
What are Signals? How do they differ from RxJS?
Signals (v16+) are a fine-grained reactivity primitive: a wrapper around a value that notifies consumers when it changes, enabling precise, zone-less change detection.
vs RxJS: signals are synchronous, always hold a current value, and are ideal for local component state. RxJS is for streams / async events over time. They interoperate via toSignal / toObservable.
count = signal(0);
double = computed(() => this.count() * 2);
effect(() => console.log(this.count()));
this.count.set(1); // or .update(v => v + 1)
What are Standalone Components and the new Angular Control Flow?
Standalone Components are Angular components that don't require an NgModule. Instead, they declare their own dependencies using the imports property, making applications simpler, easier to maintain, and better suited for lazy loading.
Angular's new built-in control flow (@if, @for, @switch, @let, and @defer) replaces structural directives like *ngIf and *ngFor with a faster, more readable syntax.
@Component({
selector: 'app-home',
standalone: true,
imports: [CommonModule],
template: `
@if (users.length) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
} @else {
<p>No users found.</p>
}
`
})
export class HomeComponent {}
What is View Encapsulation?
Controls how a component's styles are scoped:
- Emulated (default) — Angular adds attribute selectors so styles don't leak; no real Shadow DOM.
- ShadowDom — uses native Shadow DOM for true isolation.
- None — styles are global.
@Component({ encapsulation: ViewEncapsulation.Emulated })
AOT vs JIT compilation.
AOT (Ahead-of-Time) compiles templates at build time — smaller bundles, faster rendering, template errors caught early, safer. It's the default for production.
JIT (Just-in-Time) compiles in the browser at runtime — historically used in development.
What does the static flag on @ViewChild do?
@ViewChild(ref, { static: true }) resolves the query before change detection, so it's available in ngOnInit — use only for elements that are always present.
static: false (default) resolves after the view initialises, available in ngAfterViewInit — required if the element is inside an *ngIf/*ngFor.
@ViewChild('chart', { static: false }) chart!: ElementRef;
What are @HostListener and @HostBinding?
@HostListener listens to events on the host element and executes a method when the event occurs. @HostBinding binds a property, attribute, class, or style of the host element to a class property. They are commonly used in custom directives to interact with the host element without directly manipulating the DOM.
@HostBinding('class.open') isOpen = false;
@HostListener('click')
toggle() {
this.isOpen = !this.isOpen;
}
Difference between ng-template, ng-container, and ng-content?
- ng-template defines a template that is not rendered until Angular explicitly creates it.
- ng-container groups elements without creating an extra DOM element, making it useful with structural directives.
- ng-content projects content provided by a parent component into a child component.
<ng-container *ngIf="isLoggedIn">
<h2>Welcome</h2>
</ng-container>
<ng-template #loading>
Loading...
</ng-template>
<!-- Child Component -->
<ng-content></ng-content>
Why is the async pipe preferred over manual subscription?
The async pipe subscribes to an Observable/Promise, renders the latest value, and automatically unsubscribes when the component is destroyed — eliminating a whole class of memory leaks.
It also plays well with OnPush: an emission marks the component for check.
<div *ngIf="user$ | async as user">{{ user.name }}</div>
Why use Renderer2 instead of direct DOM access?
Renderer2 abstracts DOM manipulation so your code works in environments without a real DOM (server-side rendering, web workers) and keeps Angular's security model intact.
Avoid document / nativeElement.innerHTML directly — they break SSR and open XSS risks.
constructor(private r: Renderer2, private el: ElementRef) {}
ngOnInit() { this.r.addClass(this.el.nativeElement, 'active'); }
What is an NgModule and what are its main metadata fields?
An @NgModule groups related code into a cohesive block. Key fields:
- declarations — components, directives, pipes owned by this module.
- imports — other modules whose exports this module needs.
- exports — what this module makes available to importers.
- providers — services (module-level DI).
- bootstrap — the root component (AppModule only).
What does ChangeDetectorRef do (markForCheck, detectChanges, detach)?
ChangeDetectorRef gives you manual control over change detection, mainly with OnPush:
- markForCheck() — marks this component and ancestors to be checked in the next cycle (async data updated outside Angular).
- detectChanges() — runs change detection on this component subtree immediately.
- detach() / reattach() — remove/add the component from the CD tree for extreme perf tuning.
constructor(private cdr: ChangeDetectorRef) {}
update() { this.data = next; this.cdr.markForCheck(); }
What is an InjectionToken and why use one?
InjectionToken is a unique dependency injection token used for values that don't have a runtime class, such as interfaces, configuration objects, strings, numbers or feature flags. Since interfaces are removed during TypeScript compilation, Angular cannot use them as DI tokens. InjectionToken provides a type-safe way to inject these values.
export const API_URL = new InjectionToken<string>('API_URL');
providers: [
{
provide: API_URL,
useValue: 'https://api.example.com'
}
];
constructor(@Inject(API_URL) private apiUrl: string) {}
Explain useClass, useValue, useExisting, useFactory.
Angular providers tell the Dependency Injection (DI) system what object or value to provide when a dependency is requested.
- useClass – Create an instance of the specified class. Useful for replacing one service implementation with another.
- useValue – Provide a fixed value such as a configuration object, string, or number.
- useExisting – Make one token use an existing service instance instead of creating a new one.
- useFactory – Call a factory function to create the value. Useful when the value depends on runtime conditions or other services.
Use multi: true when multiple providers should be registered under the same token, such as HTTP interceptors.
// useClass
{ provide: Logger, useClass: ConsoleLogger }
// useValue
{ provide: API_URL, useValue: 'https://api.example.com' }
// useExisting
{ provide: OldLogger, useExisting: Logger }
// useFactory
{
provide: API_URL,
useFactory: () => environment.apiUrl
}
How do you create a custom pipe?
Create a class that implements PipeTransform and decorate it with @Pipe. Implement the transform() method to receive the input value and optional arguments, then return the transformed result. Custom pipes should be pure and free of side effects.
@Pipe({
name: 'truncate',
standalone: true
})
export class TruncatePipe implements PipeTransform {
transform(value: string, max = 20): string {
return value.length > max ? value.slice(0, max) + '...' : value;
}
}
How do you build a custom attribute directive?
Create a class decorated with @Directive and provide a selector. Attribute directives modify the appearance or behavior of an existing element. Use @HostListener to respond to host events, @HostBinding to update the host element, and @Input to accept values from the parent component.
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
@HostBinding('style.backgroundColor')
background = '';
@HostListener('mouseenter')
onMouseEnter() {
this.background = 'yellow';
}
@HostListener('mouseleave')
onMouseLeave() {
this.background = '';
}
}
How do custom structural directives work (TemplateRef + ViewContainerRef)?
A structural directive changes the DOM by adding or removing elements. Angular passes the directive a TemplateRef, which represents the HTML template, and a ViewContainerRef, which represents the place where that template should be rendered. The directive can then decide whether to create or remove the view. This is how *ngIf works internally.
@Directive({
selector: '[appIf]',
standalone: true
})
export class IfDirective {
constructor(
private template: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
@Input() set appIf(show: boolean) {
this.viewContainer.clear();
if (show) {
this.viewContainer.createEmbeddedView(this.template);
}
}
}
Input setter vs ngOnChanges — when to react to input changes?
Both are used to react when an @Input value changes. An Input setter is ideal for handling or transforming a single input. ngOnChanges receives a SimpleChanges object containing all changed inputs, making it the better choice when logic depends on multiple inputs.
private _id = 0;
@Input()
set id(value: number) {
this._id = value;
this.reload(value);
}
get id() {
return this._id;
}
How do you make a custom two-way bindable property?
A custom two-way binding is created using an @Input() property and an @Output() event named <property>Change. Angular's [(property)] syntax is simply a shortcut for combining property binding and event binding. In Angular 17.2+, the model() API provides a simpler way to create two-way bindable properties.
// child.component.ts
@Input() value = 0;
@Output() valueChange = new EventEmitter<number>();
update(newValue: number) {
this.value = newValue;
this.valueChange.emit(newValue);
}
// parent.component.html
<app-counter [(value)]="count"></app-counter>
What is a FormArray and when do you use it?
FormArray is a collection of form controls or form groups stored as an indexed array. It is used when the number of form fields is dynamic and can change at runtime, such as multiple phone numbers, addresses, or order items. You can add or remove controls using methods like push() and removeAt().
phones = this.fb.array([
this.fb.control('')
]);
addPhone() {
this.phones.push(this.fb.control(''));
}
removePhone(index: number) {
this.phones.removeAt(index);
}
How do you write sync and async custom validators?
A synchronous (sync) validator immediately checks a form control and returns either ValidationErrors or null. An asynchronous (async) validator performs validation that takes time, such as calling a server, and returns an Observable or Promise that eventually emits ValidationErrors or null. Sync validators are used for local validation, while async validators are commonly used to check values like whether a username or email already exists.
// Sync validator
function noSpaces(control: AbstractControl): ValidationErrors | null {
return /\s/.test(control.value)
? { spaces: true }
: null;
}
const username = new FormControl('', {
validators: [Validators.required, noSpaces]
});
How do you read route params — snapshot vs observable?
Inject ActivatedRoute to access route parameters. Use snapshot.paramMap when the parameter is read only once during component initialization. Use the paramMap observable when the same component can remain active while route parameters change, allowing the component to react automatically.
// One-time read
const id = this.route.snapshot.paramMap.get('id');
// React to parameter changes
this.route.paramMap.subscribe(params => {
const id = params.get('id');
this.load(id);
});
What does a modern functional route guard look like?
Modern Angular applications use CanActivateFn to create functional route guards. Instead of a class, a guard is simply a function that uses inject() to access services. A guard can return true, false, a UrlTree, an Observable, or a Promise.
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn()
? true
: router.createUrlTree(['/login']);
};
How do you handle HttpClient errors and retries?
Pipe the request through retry(n) for transient failures and catchError to map/handle the error (log, show a message, return a fallback). Centralise cross-cutting handling in an HTTP interceptor.
this.http.get<User>(url).pipe(
retry(2),
catchError(err => { this.notify(err); return of(null); })
)
What are route preloading strategies?
Lazy routes load on demand. A preloading strategy fetches some lazy bundles in the background after the app boots, so navigation feels instant. Built-in PreloadAllModules, or a custom strategy that preloads only routes flagged in their data.
provideRouter(routes, withPreloading(PreloadAllModules))
ViewChild vs ViewChildren (and QueryList).
@ViewChild retrieves the first matching element, directive, or component from the view. @ViewChildren returns a QueryList containing all matching elements. QueryList automatically updates when the view changes, and you can subscribe to its changes observable to react to items being added or removed.
@ViewChildren(ItemComponent)
items!: QueryList<ItemComponent>;
ngAfterViewInit() {
this.items.changes.subscribe(() => {
console.log(this.items.length);
});
}
How do you load a component dynamically?
Normally, components are declared in the template. Dynamic component loading allows Angular to create a component at runtime instead. This is useful when the component to display is not known until the application is running, such as dialogs, dashboards, or plugin systems. In modern Angular, use ViewContainerRef.createComponent() to create the component. The older ComponentFactoryResolver approach is deprecated.
@ViewChild('container', { read: ViewContainerRef })
container!: ViewContainerRef;
const ref = this.container.createComponent(MyWidgetComponent);
ref.setInput('title', 'Hello');
How does Angular's animation system work?
Angular animations are created using @angular/animations. You define animation triggers with states and transitions, then bind them in the template using [@triggerName]. Special transitions like :enter and :leave are commonly used for elements being added or removed from the DOM.
trigger('fade', [
transition(':enter', [
style({ opacity: 0 }),
animate('200ms ease-in', style({ opacity: 1 }))
])
])
What is SSR (Server-Side Rendering) and Hydration in Angular?
Server-Side Rendering (SSR) renders an Angular application into HTML on the server before sending it to the browser. This improves SEO, reduces the time to display content, and provides a faster initial page load. Hydration is the process where the browser reuses the HTML generated by the server and attaches Angular's event listeners instead of rebuilding the entire page. This avoids unnecessary DOM recreation and prevents UI flickering.
// Enable client hydration
bootstrapApplication(AppComponent, {
providers: [
provideClientHydration()
]
});
What are deferrable views (@defer)?
@defer (introduced in Angular 17) delays loading and rendering part of a template until it is needed. Instead of downloading everything during the initial page load, Angular loads deferred content later based on triggers such as on viewport, on idle, on interaction, or on hover. This reduces the initial bundle size and improves application performance.
@defer (on viewport) {
<heavy-chart />
} @placeholder {
<div>Loading chart...</div>
} @loading {
<div>Please wait...</div>
} @error {
<div>Failed to load chart.</div>
}
What are signal inputs (input()) in Angular?
input() is the signal-based replacement for @Input(). It creates a read-only signal that automatically updates when the parent passes a new value. Because it is a signal, it works naturally with computed() and effect(), reducing the need for ngOnChanges.
name = input.required<string>();
fullName = computed(() =>
`Hello ${this.name()}`
);
What are signal queries in Angular?
Signal queries are the signal-based versions of @ViewChild, @ViewChildren, @ContentChild, and @ContentChildren. Instead of decorators, they return signals whose values automatically update when the queried element or component changes.
button = viewChild<ElementRef>('btn');
effect(() => {
console.log(this.button());
});
How does Angular protect against XSS?
Cross-Site Scripting (XSS) is an attack where malicious JavaScript is injected into a web page. Angular helps prevent XSS by treating all bound values as untrusted and automatically sanitizing them based on the binding context (HTML, URL, Style, etc.). Interpolation ({{ }}) automatically escapes HTML, and bindings such as [innerHTML] are sanitized before being rendered. Only use DomSanitizer.bypassSecurityTrust... when you completely trust the content.
// Angular sanitizes the HTML before rendering
<div [innerHTML]="userHtml"></div>
Smart (container) vs dumb (presentational) components.
Smart/container components fetch data, hold state, and talk to services/the store. Dumb/presentational components just take @Inputs and emit @Outputs — no dependencies, easy to test and reuse. This separation keeps UI reusable and logic centralised.
What is ControlValueAccessor and when do you implement it?
ControlValueAccessor (CVA) is an Angular interface that allows a custom component to behave like a built-in form control such as an input or select. After implementing it, the component works with [(ngModel)], formControl, formControlName, validation, and form APIs.
It requires four methods: writeValue() updates the view from the model, registerOnChange() notifies Angular when the value changes, registerOnTouched() marks the control as touched, and setDisabledState() updates the disabled state.
@Component({
selector: 'app-rating',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: RatingComponent,
multi: true
}]
})
export class RatingComponent implements ControlValueAccessor {
value = 0;
onChange = (_: number) => {};
onTouched = () => {};
writeValue(value: number): void {
this.value = value;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
// update disabled state
}
setRating(value: number) {
this.value = value;
this.onChange(value);
this.onTouched();
}
}
How do you validate across multiple fields (e.g. password match)?
Cross-field validation is used when a validation rule depends on more than one form field, such as checking whether the password and confirm password match. Since a single FormControl can only access its own value, the validator should be added to the FormGroup, where it can access all sibling controls.
function matchPasswords(group: AbstractControl): ValidationErrors | null {
const password = group.get('password')?.value;
const confirm = group.get('confirmPassword')?.value;
return password === confirm
? null
: { passwordMismatch: true };
}
const form = this.fb.group(
{
password: [''],
confirmPassword: ['']
},
{
validators: matchPasswords
}
);
What are typed reactive forms (v14+)?
Since Angular 14, reactive forms are strongly typed. FormControl, FormGroup, and FormArray preserve the value types, providing better type safety, autocomplete, and compile-time error checking. Use nonNullable: true to prevent a control's value from being null.
email = new FormControl('', {
nonNullable: true,
validators: [Validators.email]
});
// email.value is string
How do you react to form changes reactively?
Every control/group exposes valueChanges and statusChanges as Observables. Pipe them (e.g. debounceTime + switchMap) for autosave, dependent fields, or live search. Set updateOn: 'blur' to reduce noise.
this.form.get('country')!.valueChanges.pipe(
switchMap(c => this.api.getCities(c))
).subscribe(cities => this.cities = cities);
What does updateOn: 'blur' | 'submit' do?
The updateOn option controls when a form control updates its value and runs validation. By default ('change'), updates happen on every keystroke. With 'blur', updates occur when the input loses focus. With 'submit', updates and validation run only when the form is submitted.
const email = new FormControl('', {
validators: [Validators.required, Validators.email],
updateOn: 'blur'
});
What is a selector in Angular and why use it?
The selector is a property in the @Component decorator that defines how the component is identified in a template. It acts like a custom HTML tag, so Angular knows where to render the component.
Selectors can be an element (app-user), an attribute ([appHighlight]), or a class.
@Component({ selector: 'app-user', templateUrl: './user.html' })
// used as: <app-user></app-user>
What is the entry point of an Angular application?
main.ts is the entry point. It bootstraps the app — historically platformBrowserDynamic().bootstrapModule(AppModule), and in modern standalone Angular bootstrapApplication(AppComponent). This initialises the root and renders the app into the browser.
// modern standalone bootstrap
bootstrapApplication(AppComponent, { providers: [provideRouter(routes)] });
What is the use of router-outlet?
<router-outlet> is a placeholder in a template where the router renders the component that matches the current route. As the URL changes, Angular swaps the component shown at that outlet. You can also have named/nested outlets.
<nav>…</nav>
<router-outlet></router-outlet> <!-- routed component renders here -->
How do you call a REST API in Angular?
Use the HttpClient service (provide it with provideHttpClient()). Wrap the calls in a dedicated service that returns Observables, and handle the HTTP verbs (GET/POST/PUT/DELETE) with proper error handling via catchError. Components subscribe (ideally through the async pipe).
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
getUsers() { return this.http.get<User[]>('/api/users'); }
create(u: User) { return this.http.post<User>('/api/users', u); }
}
How do you make an API call that depends on another call's response?
Chain them with switchMap so the second request uses the first's result (and stale first-calls are cancelled). For independent parallel calls use forkJoin; for ordered writes use concatMap.
this.api.getUser(id).pipe(
switchMap(user => this.api.getOrders(user.id))
).subscribe(orders => this.orders = orders);
What is the difference between a component and a directive?
A component is a directive with a template — it controls a view. A directive has no template; it adds behaviour or changes the appearance/structure of existing elements (attribute directives like ngClass, structural directives like *ngIf).
Why do we use services in Angular?
- Encapsulate reusable business logic and API calls.
- Share data/state between components (often via a BehaviorSubject).
- Provide a single instance (singleton) through dependency injection.
- Enforce separation of concerns — keep components focused on the view.
What are Angular route guards and their types?
Guards control access to and navigation between routes.
- CanActivate — allow/deny entering a route (auth).
- CanDeactivate — confirm leaving (e.g. unsaved-changes prompt).
- Resolve — pre-fetch data before the route activates.
- CanMatch / CanLoad — control whether a lazy route is even loaded.
export const authGuard: CanActivateFn = () =>
inject(AuthService).isLoggedIn() || inject(Router).createUrlTree(['/login']);
How would you debug "Cannot read properties of undefined" in Angular?
It means you accessed a property on something that isn't ready/loaded yet (often async data rendered before it arrives). Fixes:
- Guard the template with
*ngIf="data" or the safe-navigation operator data?.name. - Initialise the variable before use; check the async flow with the
async pipe. - Use the browser devtools/stack trace to find the exact line and inspect the value.
<div>{{ user?.profile?.name }}</div>
<div *ngIf="user as u">{{ u.name }}</div>
What are the options for state management in Angular?
- Service + BehaviorSubject — simple shared state for small/medium apps.
- Signals — modern, fine-grained local/shared state.
- NgRx / NgRx SignalStore — structured Redux-style store for large apps needing traceability and DevTools.
constructor vs ngOnInit — what's the difference?
The constructor is a TypeScript/JS class feature that runs when the class is instantiated — Angular uses it only to inject dependencies. ngOnInit is an Angular lifecycle hook that runs after the first change detection, once @Input values are set.
Rule of thumb: inject in the constructor, initialise in ngOnInit (data fetching, setup that needs inputs).
constructor(private api: ApiService) {} // DI only
ngOnInit() { this.user = this.api.getUser(this.id); } // id is ready here
What is trackBy in *ngFor and why use it?
By default *ngFor tracks list items by object identity, so if the array reference changes (e.g. after an HTTP refresh) Angular destroys and recreates every DOM node. A trackBy function tells Angular how to identify an item (usually by id), so it only re-renders the rows that actually changed.
<li *ngFor="let u of users; trackBy: trackById">{{ u.name }}</li>
trackById(index: number, u: User) { return u.id; }
// v17 control flow: @for (u of users; track u.id) { ... }
forRoot() vs forChild() — what's the difference?
forRoot() is called once in the root module to register singleton services and application-wide configuration. forChild() is used in feature modules to register routes or components without creating another instance of those services.
A common example is RouterModule.forRoot() in the root module and RouterModule.forChild() in feature modules.
// app.module.ts
RouterModule.forRoot(appRoutes)
// feature.module.ts
RouterModule.forChild(featureRoutes)
What causes ExpressionChangedAfterItHasBeenCheckedError?
In dev mode Angular runs change detection twice and compares. If a bound value changed between the two passes — usually because you mutated it in a lifecycle hook like ngAfterViewInit that runs after the view was already checked — it throws this error to warn of an inconsistent view.
// Fixes:
// 1) move the update earlier (ngOnInit)
// 2) defer it: setTimeout(() => this.value = x) or a microtask
// 3) cdRef.detectChanges() after the change
Observable vs Promise — what's the difference?
- Promise — a single future value; eager (runs immediately); not cancellable; no operators.
- Observable — a stream of 0..∞ values over time; lazy (runs only on
subscribe); cancellable via unsubscribe; rich operators (map, switchMap, debounceTime).
Angular's HttpClient returns Observables, which is why they dominate Angular apps.
// promise: fires now, one value
fetch('/api').then(r => r.json());
// observable: fires on subscribe, cancellable
this.http.get('/api').subscribe(data => ...);
How would you handle rapid user actions (e.g. live search or AI analysis requests) to avoid multiple API calls and outdated results?
Use debounceTime() to wait until the user stops typing or clicking for a short duration, reducing unnecessary API calls. Then use switchMap() to automatically cancel any previous pending request whenever a new request is triggered. This ensures that only the latest response is processed and displayed.
this.search$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => this.api.search(query))
).subscribe(result => {
this.results = result;
});
Your page displays 10,000 records and scrolling becomes very slow. How would you optimize it?
First, use trackBy so Angular reuses existing DOM elements instead of recreating them. For very large datasets, use CDK Virtual Scroll so only the visible items are rendered. Combine this with ChangeDetectionStrategy.OnPush to reduce unnecessary change detection.
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let user of users; trackBy: trackById">
{{ user.name }}
</div>
</cdk-virtual-scroll-viewport>
trackById(index: number, user: User) {
return user.id;
}
How do you prevent memory leaks caused by RxJS subscriptions?
Always unsubscribe from long-lived Observables when the component is destroyed. In modern Angular, the recommended approach is using takeUntilDestroyed(). In older versions, use takeUntil() with a Subject or rely on the async pipe whenever possible.
this.userService.users$
.pipe(takeUntilDestroyed())
.subscribe(users => this.users = users);
How would you prevent users from submitting the same form multiple times by clicking the Save button repeatedly?
Disable the Save button while the request is in progress or use exhaustMap(). It ignores subsequent button clicks until the current request completes, preventing duplicate submissions.
fromEvent(saveBtn, 'click')
.pipe(
exhaustMap(() => this.api.save(data))
)
.subscribe();
How would you share data between unrelated Angular components?
For unrelated components, avoid directly referencing each other. Use a shared service with an RxJS BehaviorSubject or use NgRx if the state is shared across multiple features. Parent-child communication should still use @Input() and @Output().
@Injectable({ providedIn: 'root' })
export class UserService {
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
setUser(user: User) {
this.userSubject.next(user);
}
}
Your Angular application has become slow because the initial bundle size is very large. How would you improve the loading performance?
I would split the application into feature modules and lazy load them using the Angular Router. This ensures only the modules required for the current route are downloaded initially, reducing the bundle size and improving the application's startup time.
const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.module')
.then(m => m.AdminModule)
}
];
Multiple components call the same API, causing duplicate network requests. How would you optimize it?
I would cache the API response inside a shared service using shareReplay(1). The first component triggers the HTTP request, while subsequent subscribers receive the cached response without making additional network calls.
users$ = this.http.get<User[]>('/api/users').pipe(
shareReplay(1)
);
How would you prevent unauthenticated users from accessing protected pages?
I would use an Angular Route Guard such as CanActivate. Before navigation, the guard checks whether the user is authenticated. If not, it redirects the user to the login page.
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn()
? true
: router.createUrlTree(['/login']);
};
A component is re-rendering frequently and performance is poor. How would you optimize Angular Change Detection?
I would use ChangeDetectionStrategy.OnPush. With OnPush, Angular checks the component only when an @Input() reference changes, an event occurs, or an Observable emits. This significantly reduces unnecessary change detection cycles.
@Component({
selector: 'app-users',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UsersComponent {}
How would you implement an auto-save feature for a form without sending an API request on every keystroke?
I would subscribe to form.valueChanges, apply debounceTime() to wait until the user stops typing, use distinctUntilChanged() to avoid duplicate requests, and finally use switchMap() to cancel any pending save request when new changes occur.
this.form.valueChanges.pipe(
debounceTime(1000),
distinctUntilChanged(),
switchMap(value => this.api.saveDraft(value))
).subscribe();
How would you handle HTTP errors consistently across the entire Angular application?
I would create an HTTP Interceptor. It centralizes error handling, logging, authentication failures, and token refresh logic instead of duplicating the same code inside every component or service.
intercept(req: HttpRequest<any>, next: HttpHandler) {
return next.handle(req).pipe(
catchError(error => {
console.error(error);
return throwError(() => error);
})
);
}
How would you handle an expired JWT token without forcing the user to log in again?
I would detect a 401 Unauthorized response inside an HTTP Interceptor, call the Refresh Token API, store the new access token, retry the original request, and only redirect to login if the refresh request also fails.
401 Error
↓
Refresh Token API
↓
New Access Token
↓
Retry Original Request
How would you display upload progress while uploading a large file?
Angular's HttpClient supports upload progress events. By enabling reportProgress and observe: 'events', I can calculate the upload percentage and display a progress bar to the user.
this.http.post(url, file, {
reportProgress: true,
observe: 'events'
}).subscribe(event => {
if (event.type === HttpEventType.UploadProgress) {
const progress = Math.round(100 * event.loaded / event.total!);
}
});
What are microfrontends and how do they differ from a normal frontend?
A microfrontend architecture splits one web app into independently built, tested and deployed pieces, each owned end-to-end by a different team, and composed together in the browser at runtime.
A normal (monolithic) frontend is one codebase, one build, one deploy — every team's change ships in the same bundle, on the same release train.
The real difference isn't technical, it's organisational: microfrontends trade runtime and tooling complexity for independent deployability. In Angular this is usually done with Module Federation (Webpack 5 / Native Federation), where a shell loads remotes at runtime.
// shell — webpack.config.js
new ModuleFederationPlugin({
remotes: {
orders: 'orders@https://orders.app.com/remoteEntry.js'
},
shared: {
'@angular/core': { singleton: true, strictVersion: true },
'@angular/router': { singleton: true, strictVersion: true }
}
});
// shell route — the remote is fetched only when visited
{
path: 'orders',
loadChildren: () => import('orders/Module').then(m => m.OrdersModule)
}
How do you build a scalable enterprise Angular application?
First, define the axis — this is the trap. On a frontend, "scalable" almost never means server traffic; it means the codebase and the team can keep growing without the app rotting.
- Code scale — feature-based structure with enforced boundaries, so 200k lines stay navigable.
- Team scale — clear ownership; teams don't collide in the same files.
- Runtime scale — lazy loading, OnPush, virtual scrolling, performance budgets so it stays fast as features pile on.
- Data scale — pagination and virtualisation; never render 10k rows.
// Nx — enforce boundaries so 'scalable' is checked by the linter, not a wiki page
"@nx/enforce-module-boundaries": ["error", {
"depConstraints": [
{ "sourceTag": "scope:orders", "onlyDependOnLibsWithTags": ["scope:orders", "scope:shared"] },
{ "sourceTag": "type:feature", "onlyDependOnLibsWithTags": ["type:ui", "type:data-access"] }
]
}]
Walk me through the architecture of your Angular application.
Answer in layers, and give a reason for each choice — the interviewer is testing whether you designed it or inherited it.
- Structure —
core/ (singletons: auth, interceptors, error handling), shared/ (dumb reusable UI), features/ (one lazy-loaded folder per domain). - Components — smart/container components fetch data; dumb/presentational ones take
@Input and emit @Output, so they're trivially testable. - Services + DI — HTTP and business logic live in services, never components.
- State — local first; a store only where state is genuinely shared.
- Cross-cutting — interceptors for auth/errors, guards for routes.
src/app/
core/ # provided once: interceptors, guards, auth
shared/ # dumb UI, pipes, directives — no feature imports
features/
orders/
data-access/ # OrdersService, models
feature-list/ # smart component (routed)
ui/ # dumb components
app.routes.ts # lazy: loadChildren / loadComponent
What's new in Angular 14, 15 and 16? What did migrating between them involve?
- Angular 14 — standalone components (developer preview), typed reactive forms, the
inject() function usable in functions, protected route guards as plain functions, extended template diagnostics. - Angular 15 — standalone APIs stable (loadComponent routing, functional guards/interceptors become the norm), directive composition API, NgOptimizedImage stable, MDC-based Material.
- Angular 16 — Signals (developer preview),
takeUntilDestroyed + DestroyRef, required inputs (@Input({required: true})), route params as component inputs, esbuild dev server preview, non-destructive hydration for SSR.
Migration is ng update @angular/core@15 @angular/cli@15 per major — schematics rewrite most breaking changes; the practical work is usually TypeScript/Node version bumps, Material MDC style breakages (v15), and dependency alignment.
# one major at a time, run the schematics
ng update @angular/core@15 @angular/cli@15
ng update @angular/material@15 # the usual pain point (MDC)
*ngIf vs [hidden] — what's the difference?
*ngIf adds/removes the element from the DOM: when false, the component is destroyed (ngOnDestroy runs, subscriptions in it die, no change detection cost). [hidden] just toggles display: none — the element stays in the DOM, stays instantiated, and keeps being change-detected.
- Expensive component, rarely shown →
*ngIf. - Cheap element toggled constantly (avoid re-init cost / preserve internal state like scroll or form input) →
[hidden].
<heavy-chart *ngIf="showChart"></heavy-chart> <!-- destroyed when false -->
<div [hidden]="collapsed">cheap, keeps DOM state</div> <!-- display:none only -->
setValue vs patchValue?
Both write values into a FormGroup/FormControl:
setValue(obj) — strict, full shape required: the object must contain a value for every control; missing or extra keys throw. Use when the payload should exactly match the form.patchValue(obj) — lenient, partial update: sets what's provided, silently ignores the rest (including typos in key names!).
Both fire valueChanges; pass { emitEvent: false } to update silently (crucial when writing from inside a valueChanges handler to avoid loops).
this.form.setValue({ name: u.name, email: u.email }); // must be complete
this.form.patchValue({ email: u.email }); // partial is fine
this.form.patchValue({ email: x }, { emitEvent: false }); // no valueChanges loop
dirty vs touched vs pristine? valid/invalid/pending? How do you enable/disable controls reactively?
Two independent state axes on every AbstractControl:
- Value axis:
pristine (never changed) ↔ dirty (value changed by the user). - Visit axis:
untouched ↔ touched (control was blurred).
Validity: valid / invalid / pending (async validator running) / disabled (excluded from validation and from form.value!).
Enable/disable via API — ctrl.disable() / ctrl.enable(), typically inside a valueChanges subscription; never [disabled] in the template with reactive forms (Angular warns).
this.form.get('country')!.valueChanges.subscribe(c => {
const state = this.form.get('state')!;
c === 'IN' ? state.enable() : state.disable(); // reactive, not [disabled]
});
// template:
// <div *ngIf="email.invalid && (email.dirty || email.touched)">...
routerLink vs router.navigate() vs navigateByUrl()?
- routerLink — declarative, in templates: renders a real
<a href> (middle-click/SEO friendly); pairs with routerLinkActive. - router.navigate(['users', id]) — programmatic, takes a commands array; supports relative navigation via
{ relativeTo: this.route }, plus queryParams, fragment, state. - router.navigateByUrl('/users/42') — programmatic, takes an absolute URL string, always from root; query params go inside the string.
Rule of thumb: template link → routerLink; navigate after logic (form saved, login done) → navigate(); redirect to a fully-formed URL (e.g. stored returnUrl) → navigateByUrl.
// after login: go back where the guard bounced from
const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl') ?? '/';
this.router.navigateByUrl(returnUrl);
// relative: /orders/42 -> /orders/42/items
this.router.navigate(['items'], { relativeTo: this.route });
Child routes, named (auxiliary) outlets, and the wildcard route — how do they work?
Child routes — nested children: [] render into the parent component's own <router-outlet>: layout stays, only the inner area swaps (tabs, master-detail).
Named outlets — <router-outlet name="panel"> + outlet: 'panel' on the route lets independent content (chat sidebar, preview pane) have its own URL segment: /inbox(panel:compose).
Wildcard — path: '**' catches everything unmatched (404 page). Route order matters: first match wins, so ** must be last and specific paths go before parameterized ones.
const routes: Routes = [
{ path: 'orders', component: OrdersShell, children: [
{ path: '', component: OrderList },
{ path: ':id', component: OrderDetail }, // renders inside OrdersShell's outlet
]},
{ path: 'compose', component: ComposeCmp, outlet: 'panel' },
{ path: '**', component: NotFoundCmp }, // ALWAYS last
];
PathLocationStrategy vs HashLocationStrategy?
- PathLocationStrategy (default) — clean URLs (
/users/42) via the HTML5 History API. Requires the server to serve index.html for every route (a rewrite/fallback rule) — otherwise a refresh or deep link on /users/42 is a 404. - HashLocationStrategy — routes after a hash (
/#/users/42). The browser never sends the fragment to the server, so no server config needed — but URLs are uglier, and SEO/SSR are out.
# nginx fallback for PathLocationStrategy
location / {
try_files $uri $uri/ /index.html;
}
// hash only when you can't touch the server (file://, legacy hosting):
provideRouter(routes, withHashLocation())
How do you pass data between routes? And what is RouteReuseStrategy?
Options by durability:
- Route/query params — survives refresh and sharing; the default for identity (
/orders/42?tab=items). Pass ids, not objects. - Navigation state —
router.navigate([...], { state: { draft } }), read via history.state. Invisible in the URL but lost on refresh — good for optional context only. - Shared service / store — for real data; the target route should still be able to fetch by id if state is empty.
- Route data / resolvers — static config or prefetched data attached to the route.
RouteReuseStrategy (awareness): by default Angular destroys a component on leave and recreates on return; a custom strategy can detach and store the component (e.g. keep a filtered list's state and scroll when going list → detail → back).
// list -> detail keeping context out of the URL
this.router.navigate(['/orders', id], { state: { from: 'search' } });
// detail:
const from = this.router.getCurrentNavigation()?.extras.state?.['from']
?? history.state.from; // after refresh: undefined -> fall back gracefully
What's the order of multiple interceptors? And the HttpParams/HttpHeaders immutability common Mistakes?
Interceptors run in registration order for requests, reverse order for responses — an onion: first registered = outermost layer. Register auth before logging and the log sees the token; order is the array order in withInterceptors([auth, logging]) (or multi-provider order for class-based ones).
Immutability gotcha: HttpRequest, HttpHeaders, HttpParams are all immutable — headers.set(...) and params.append(...) return a new object. Calling them without using the return value silently does nothing.
// WRONG — result thrown away:
// req.headers.set('Authorization', token);
// RIGHT — clone with the new immutable pieces:
const authed = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
return next(authed);
let params = new HttpParams().set('page', '1');
params = params.set('size', '20'); // reassign!
What is NgZone.runOutsideAngular() and when do you use it? (And web workers?)
Zone.js triggers change detection after every patched async event. runOutsideAngular(fn) runs code where those events don't trigger CD — for high-frequency work that doesn't change bound state on every tick: mousemove/scroll listeners, requestAnimationFrame loops, polling timers, third-party libs (charts, maps).
When a result does need to reach the UI, re-enter with zone.run(() => ...).
Web workers (awareness): for CPU-heavy computation (parsing, crunching) move the work off the main thread entirely — ng generate web-worker scaffolds one; communication is postMessage.
constructor(private zone: NgZone) {}
ngOnInit() {
this.zone.runOutsideAngular(() => {
fromEvent(window, 'mousemove').pipe(throttleTime(50)).subscribe(e => {
this.updateCanvas(e); // no CD per event
if (this.crossedThreshold(e))
this.zone.run(() => this.showHint = true); // CD only when UI must change
});
});
}
What is a PWA? How does the Angular service worker cache assets vs API calls?
A Progressive Web App is a web app that's installable, offline-capable and fast, built on three pillars: HTTPS, a web app manifest (name, icons — enables install), and a service worker (a background script that intercepts network requests and serves from cache).
ng add @angular/pwa wires Angular's service worker, configured via ngsw-config.json:
- assetGroups — app shell files (js/css/index.html):
prefetch (download all at install) or lazy; versioned by content hash, updated as a whole so the app is always a consistent build. - dataGroups — API responses: strategy
freshness (network-first with timeout fallback to cache) or performance (cache-first until maxAge), with maxSize/maxAge per group.
// ngsw-config.json
"dataGroups": [{
"name": "api-catalog",
"urls": ["/api/catalog/**"],
"cacheConfig": {
"strategy": "performance", // cache-first
"maxSize": 100, "maxAge": "1d"
}
}]
Which Angular CLI commands do you use daily? What does ng build --configuration production actually do?
Daily: ng serve (dev server + HMR/watch), ng generate component|service|guard, ng test, ng lint, ng build, ng update, ng add (install + run a library's setup schematic).
ng build --configuration production: AOT compilation, tree-shaking + minification, style/script optimization, output hashing for cache-busting, source maps off, budgets enforced (build fails if bundles exceed angular.json limits), fileReplacements swapping in environment.prod.ts.
Schematics (awareness) are the code generators behind ng generate/add/update — libraries ship them so ng add @angular/material can edit your project, and ng update runs migration schematics.
ng build --configuration production
ng build --stats-json && npx source-map-explorer dist/**/*.js # what's IN the bundle
ng update @angular/core@16 @angular/cli@16 # migration schematics
What are the DI resolution modifiers @Self, @Optional, @SkipSelf, @Host?
They tune where the injector search looks and what happens on a miss (default: walk up from the component's own injector to root, throw if not found):
- @Optional() — inject
null instead of throwing. For "use it if present" dependencies. - @Self() — only this component/directive's own injector; don't walk up.
- @SkipSelf() — start from the parent injector; skip your own. Classic for a directive finding a parent instance of itself (nested menus), or a service checking it isn't provided twice.
- @Host() — stop the search at the host component (a directive may look at its component but not beyond).
// directive grabs its OWN ngModel, not an ancestor's:
constructor(@Self() private ngControl: NgControl) {}
// guard against double forRoot():
constructor(@Optional() @SkipSelf() parent?: CoreModule) {
if (parent) throw new Error('CoreModule imported twice');
}