Angular Interview Questions With Answers and Working Code
Prepare for Angular interview questions with concise answers, TypeScript examples, change detection details, RxJS trade-offs, and a practical coding task.

Angular interview questions usually mix framework knowledge with TypeScript, browser behavior, and application design. Strong answers stay concise: define the concept, name the trade-off, then show how you would use it in working code.
What Angular interviews usually test#
Angular interviews test whether you can build, explain, and debug a maintainable frontend. Memorizing decorators is not enough.
Expect several prompt styles:
- Verbal questions: Explain dependency injection, change detection, or Observable cancellation.
- Code reading: Predict what a template renders or find a subscription leak.
- Debugging tasks: Diagnose duplicate HTTP requests, stale views, or lifecycle timing errors.
- Component exercises: Build a form, searchable list, or parent-child interaction.
Separate framework questions from adjacent skills. Angular dependency injection is framework knowledge. Closures and structural typing belong to TypeScript. Event propagation belongs to the browser. Choosing service boundaries is application design.
A useful answer has three parts:
- Definition: “
OnPushlimits when Angular checks a component.” - Trade-off: “It rewards immutable updates but exposes accidental mutation.”
- Example: “I replace an array instead of pushing into the existing reference.”
Some Angular coding interview questions also include ordinary data structures. Use the pattern reference or Top Interview 150 list for that part of your preparation.
Angular fundamentals interview questions#
The fundamentals describe how Angular turns classes and templates into a component tree.
What is a component?#
A component owns a view. Its metadata connects a TypeScript class to a selector, template, styles, and dependencies.
@Component({
selector: 'app-counter',
standalone: true,
template: `
<button (click)="count = count + 1">Add</button>
<p [class.active]="count > 0">{{ count }}</p>
`
})
count = 0;
}This shows:
{{ count }}: interpolation[class.active]: property binding(click): event binding[(ngModel)]: two-way binding, combining value input and change output
How do directives, pipes, services, and components differ?#
Create a component when behavior needs its own view. Create a directive to add behavior to an existing element. Use a pipe for a pure display transformation. Put shared business or data-access behavior in a service.
What are modules and standalone components?#
NgModules group declarations and providers. Standalone components declare their imports directly and can be composed without an application-level declaration module. You should understand both because existing applications may use either structure.
How does Angular dependency injection work?#
Angular resolves dependencies from hierarchical injectors. A provider defines how to create or supply a value. A narrower provider scope can create separate instances.
@Injectable({ providedIn: 'root' })
@Component({ /* ... */ })
private users = inject(UserService);
}The interview-ready point is not just “DI supplies services.” It also separates construction from use, which improves substitution and testing.
Components, lifecycle hooks, and data flow#
Angular favors explicit data flow: inputs enter a component and outputs report events.
Inputs configure children. Outputs notify parents. Content projection inserts caller-owned markup through ng-content. View queries access rendered children, but they create tighter coupling and should not replace normal bindings.
The commonly discussed lifecycle order is:
ngOnChangeswhen bound inputs changengOnInitafter initial inputs are establishedngAfterViewInitafter the component view existsngOnDestroybefore removal
Use ngOnChanges for input-dependent work, ngOnInit for initialization, and ngAfterViewInit only when you need the rendered view. Clean up long-lived resources during destruction.
private subscription?: Subscription;
private onResize = () => this.measure();
ngOnInit(): void {
this.subscription = interval(1000).subscribe(() => this.refresh());
window.addEventListener('resize', this.onResize);
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
window.removeEventListener('resize', this.onResize);
}
private measure() {}
private refresh() {}
}You can also use takeUntilDestroyed for Observable teardown. Browser APIs still need their corresponding cleanup operation unless you wrap them as streams.
Change detection, signals, and performance#
Angular change detection synchronizes component state with the rendered view.
Default change detection checks broadly during an application update. OnPush narrows checks to relevant triggers such as new input references, handled events, signal updates, or explicit requests.
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `@for (user of users; track user.id) { {{ user.name }} }`
})
@Input() users: User[] = [];
rename(id: number, name: string): void {
this.users = this.users.map(user =>
user.id === id ? { ...user, name } : user
);
}
}Replacing the array gives Angular a new reference. Mutating an object in place can leave an OnPush child with a stale view.
Angular signals hold synchronous reactive state. computed derives a value and caches it until its dependencies change. effect performs side effects; it should not become a default substitute for ordinary data flow.
query = signal('');
users = signal<User[]>([]);
visibleUsers = computed(() => {
const value = this.query().toLowerCase();
return this.users().filter(user =>
user.name.toLowerCase().includes(value)
);
});Track list items by stable identity. Avoid calling expensive methods directly from templates. Signals, RxJS, and store libraries solve different problems; choose based on state lifetime, asynchronous behavior, and team conventions.
RxJS and asynchronous code questions#
RxJS questions test whether you understand time, cancellation, and ownership.
A Promise produces one eventual result. An Observable can produce no values, one value, or many values and supports teardown. Cold streams create work per subscription. Hot streams share an ongoing producer.
For request mapping:
switchMapcancels the previous inner subscription. Use it for search.mergeMapallows concurrent work. Use it when all requests may complete independently.concatMapqueues work in order.exhaustMapignores new triggers while one request is active. It can prevent repeated submissions.
Handle errors inside the level that should recover:
results$ = this.query.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query =>
this.api.search(query).pipe(
catchError(() => of([]))
)
)
);Common mistakes include nested subscriptions, subscribing multiple times to a cold HTTP pipeline, swallowing useful errors, and forgetting teardown. Prefer template consumption with async when the template owns the subscription.
Forms, routing, HTTP, and application architecture#
Choose forms and service boundaries according to complexity, not habit.
Reactive forms keep the model in TypeScript and suit dynamic validation and testing. Template-driven forms are concise for small forms.
const noWhitespace: ValidatorFn = control =>
String(control.value).trim().length
? null
: { whitespace: true };
form = new FormGroup({
name: new FormControl('', {
nonNullable: true,
validators: [Validators.required, noWhitespace]
})
});Read route parameters from ActivatedRoute. Use guards to express navigation rules, but enforce real authorization on the server. Lazy loading defers route code. Nested routes model nested layouts. Interceptors handle cross-cutting HTTP concerns such as headers or normalized error handling.
Keep API transport in data-access services. Keep view state near the component unless several features genuinely share it. As an application grows, organize around features rather than one global folder for every component and service type.
Testing and debugging questions#
Test observable behavior at the smallest useful boundary.
For an input-driven component, set the input and assert rendered output:
it('renders the input name', () => {
const fixture = TestBed.createComponent(UserCardComponent);
fixture.componentRef.setInput('name', 'Ada');
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Ada');
});For an HTTP service, assert both the request and result:
it('loads users', () => {
service.search('ada').subscribe(users =>
expect(users[0].name).toBe('Ada')
);
const request = http.expectOne('/api/users?q=ada');
expect(request.request.method).toBe('GET');
request.flush([{ id: 1, name: 'Ada' }]);
});When debugging:
- Expression-change error: Find state changed after Angular already checked that view. Move work to the correct lifecycle or redesign the data flow.
- Stale view: Check in-place mutation,
OnPushboundaries, and work performed outside Angular-managed updates. - Duplicate requests: Look for multiple subscriptions to a cold stream.
- Leaked subscriptions: Identify who creates the subscription and when that owner is destroyed.
Worked Angular coding task: a searchable user list#
Build an API-backed user list with debounced search, cancellation, loading feedback, errors, and accessible status text.
The service owns HTTP details. The component owns query and view state.
map, of, startWith, switchMap } from 'rxjs';
interface User {
id: number;
name: string;
email: string;
}
interface ViewState {
loading: boolean;
users: User[];
error: string | null;
}
@Injectable({ providedIn: 'root' })
private http = inject(HttpClient);
search(query: string): Observable<User[]> {
const params = new HttpParams().set('q', query);
return this.http.get<User[]>('/api/users', { params });
}
}
@Component({
selector: 'app-user-search',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<label for="user-search">Search users</label>
<input id="user-search" type="search" [formControl]="query">
@if (state$ | async; as state) {
<p aria-live="polite">
@if (state.loading) { Loading users }
@else if (state.error) { {{ state.error }} }
@else { {{ state.users.length }} results }
</p>
<ul>
@for (user of state.users; track user.id) {
<li>
<strong>{{ user.name }}</strong>
<span>{{ user.email }}</span>
</li>
} @empty {
@if (!state.loading && !state.error) {
<li>No users found</li>
}
}
</ul>
}
`
})
private api = inject(UserApi);
readonly query = new FormControl('', { nonNullable: true });
readonly state$ = this.query.valueChanges.pipe(
startWith(this.query.value),
map(value => value.trim()),
debounceTime(300),
distinctUntilChanged(),
switchMap(query =>
this.api.search(query).pipe(
map(users => ({ loading: false, users, error: null })),
startWith({ loading: true, users: [], error: null }),
catchError(() => of({
loading: false,
users: [],
error: 'Could not load users.'
}))
)
)
);
}switchMap cancels the previous request subscription when the query changes. The async pipe owns teardown when the component disappears. OnPush works cleanly because each state emission is a new object.
The client does constant local work before each request. Server-side filtering complexity depends on the API and its indexes, so you should not invent a bound. Rendering r returned users takes O(r) time and O(r) client space for the response and rendered list. Stable tracking lets Angular reuse existing rows when identities remain unchanged.
Frequently asked questions
- What do Angular interviews usually test?
- Angular interviews test whether you can build, explain, and debug a maintainable frontend. Prompts may cover framework knowledge, TypeScript, browser behavior, application design, code reading, debugging, and component exercises.
- How does Angular dependency injection work?
- Angular resolves dependencies through hierarchical injectors, while providers define how values are created or supplied. Separating construction from use improves substitution and testing.
- What is the difference between Angular components, directives, pipes, and services?
- A component owns a view, while a directive adds behavior to an existing element. Pipes handle pure display transformations, and services contain shared business or data-access behavior.
- How does OnPush change detection work in Angular?
- OnPush narrows checks to triggers such as new input references, handled events, signal updates, or explicit requests. Immutable updates provide new references, while in-place mutation can leave a child view stale.
- When should switchMap be used in Angular?
- Use switchMap when a new value should cancel the previous inner subscription, such as debounced search. Other mapping operators suit concurrent, ordered, or ignored repeated work.
Keep reading

React Interview Questions and Answers With Working Code
Prepare for React interviews with working code on renders, hooks, state ownership, effects, debugging, and debounced search.

Hello Interview System Design: A Practical Study Guide
Turn Hello Interview system design material into a repeatable process for clarifying requirements, drawing architectures, and defending trade-offs.

System Design Interview Questions With Worked Answers
Use a repeatable framework to clarify requirements, trace request flows, assess trade-offs, and work through a URL shortener design.