1. What is Angular and what are its key features?
Angular is an open-source, TypeScript-based front-end framework from Google for building single-page web applications. It provides components, templates, dependency injection, routing, forms, an HTTP client and a CLI out of the box. Key features include two-way data binding, standalone components, signals for fine-grained reactivity, RxJS integration, ahead-of-time compilation and strong tooling for testing and building.
2. What is the difference between AngularJS and Angular?
AngularJS (1.x) is the original JavaScript framework built on controllers, $scope and two-way digest-cycle binding, while Angular (2+) is a complete rewrite in TypeScript built on components. Angular is faster thanks to AOT compilation and a better change detection model, supports mobile and server-side rendering, and has hierarchical dependency injection. AngularJS reached end of life in January 2022.
3. What is a component in Angular?
A component is the basic building block of an Angular UI: a TypeScript class decorated with @Component that controls a view through an HTML template and styles. The decorator metadata defines the selector, template, styles and, for standalone components, the imports it needs. Components form a tree, and every Angular app has a root component.
@Component({
selector: 'app-hello',
standalone: true,
template: `<h1>Hello {{ name }}</h1>`,
})
export class HelloComponent {
name = 'Angular';
}
4. What are standalone components and how do they differ from NgModules?
Standalone components are components, directives and pipes that declare their own dependencies through the imports array instead of belonging to an NgModule. They remove the boilerplate of modules, simplify lazy loading and are the default in modern Angular (v19+). NgModules still work and can be mixed with standalone code, but new apps are bootstrapped with bootstrapApplication and provideRouter.
5. What is an NgModule and what are its main properties?
An NgModule is a class decorated with @NgModule that groups related components, directives, pipes and services into a cohesive block. Its main properties are declarations, imports, exports, providers and bootstrap. Modules were the standard way to organize apps before standalone APIs, and you still see them in legacy codebases and some libraries.
6. What is data binding in Angular and what types exist?
Data binding synchronizes data between the component class and the template. The four forms are interpolation {{ value }}, property binding [prop]="value", event binding (event)="handler()" and two-way binding [(ngModel)]="value". Interpolation and property binding flow from component to view, event binding flows from view to component, and two-way combines both.
<h1>{{ title }}</h1>
<img [src]="imageUrl">
<button (click)="save()">Save</button>
<input [(ngModel)]="name">
7. What is the difference between interpolation and property binding?
Interpolation {{ }} converts an expression to a string and inserts it into text or an attribute, while property binding [property] sets a DOM property to the actual expression value without string conversion. Use property binding for non-string values such as booleans or objects, for example [disabled]="isBusy". Both are one-way from the component to the view and both sanitize unsafe values.
8. What are directives in Angular and what are their types?
Directives are classes that add behavior to elements in the DOM. There are three types: components (directives with a template), structural directives that change the DOM layout such as *ngIf and *ngFor, and attribute directives that change the appearance or behavior of an element such as ngClass and ngStyle. You create custom ones with the @Directive decorator.
9. What is the difference between structural and attribute directives?
Structural directives add, remove or replace elements in the DOM and are prefixed with *, like *ngIf or *ngFor, while attribute directives only change the look or behavior of an existing element, like ngClass or a custom highlight directive. Modern Angular also offers built-in control flow blocks @if, @for and @switch that replace the structural directives with cleaner syntax and better performance.
@if (user) {
<p>{{ user.name }}</p>
} @else {
<p>Please log in</p>
}
10. What are pipes in Angular and how do you create a custom pipe?
Pipes transform values in a template for display, such as date, currency, uppercase and async. To create a custom pipe, decorate a class with @Pipe, give it a name and implement PipeTransform.transform(). Pipes are pure by default, so they only rerun when their input reference changes, which makes them cheap.
@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;
}
}
11. What is the difference between pure and impure pipes?
A pure pipe runs only when its input value or reference changes, so it is cached and efficient. An impure pipe, declared with pure: false, runs on every change detection cycle, which is needed when it depends on mutable state such as array contents but can hurt performance. The built-in async pipe is impure but optimized.
12. What are services and how does dependency injection work in Angular?
A service is a class that holds reusable logic or shared state, and dependency injection (DI) is the mechanism Angular uses to supply instances of it to components and other classes. You mark a class with @Injectable and request it through the constructor or the inject() function. Angular looks up the provider in the injector hierarchy and returns a shared instance.
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getUsers() { return this.http.get<User[]>('/api/users'); }
}
13. What does providedIn: 'root' mean and what are the provider scopes?
providedIn: 'root' registers the service with the root injector, making it an application-wide singleton that is also tree-shakable when unused. Other scopes are the providers array of a component or directive, which creates a separate instance per component, route-level providers, and NgModule providers. Injectors form a hierarchy, and Angular resolves a dependency by walking up from the requesting element.
14. What are the Angular component lifecycle hooks?
Lifecycle hooks let you run code at specific moments of a component's life. The main ones, in order, are ngOnChanges, ngOnInit, ngDoCheck, ngAfterContentInit, ngAfterContentChecked, ngAfterViewInit, ngAfterViewChecked and ngOnDestroy. Use ngOnInit for initialization after inputs are set, and ngOnDestroy to clean up subscriptions and timers. Newer code also uses afterNextRender and afterRender.
15. What is the difference between the constructor and ngOnInit?
The constructor is a TypeScript feature that runs when the class is instantiated and is meant for dependency injection and simple field setup. ngOnInit is an Angular lifecycle hook called once after the first ngOnChanges, when input properties are set. Put initialization logic that depends on inputs or needs the component fully configured in ngOnInit, not the constructor.
16. What is change detection in Angular and what is OnPush?
Change detection is the process by which Angular checks component state and updates the DOM when data changes, triggered by events, timers and HTTP calls through Zone.js. The default strategy checks every component in the tree. With ChangeDetectionStrategy.OnPush, a component is checked only when an input reference changes, an event fires inside it, an observable bound with async emits or a signal it reads changes, which improves performance.
@Component({
selector: 'app-item',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ item.name }}`,
})
export class ItemComponent { @Input() item!: Item; }
17. What are signals in Angular?
Signals are a reactive primitive that wrap a value and notify consumers when it changes, enabling fine-grained change detection without relying solely on Zone.js. You create writable state with signal(), derive values with computed(), and run side effects with effect(). Read a signal by calling it like a function. Angular also provides signal-based input(), output(), model() and viewChild() APIs.
count = signal(0);
double = computed(() => this.count() * 2);
increment() { this.count.update(c => c + 1); }
18. What is the difference between Observables and Promises?
A Promise handles a single future value, is eager and cannot be cancelled, while an Observable can emit multiple values over time, is lazy (nothing runs until you subscribe), is cancellable through unsubscribe and offers a rich set of RxJS operators. Angular uses Observables in HttpClient, the router, reactive forms and EventEmitter. You can convert between them with firstValueFrom() and from().
19. What is RxJS and how do you avoid memory leaks with subscriptions?
RxJS is a library for reactive programming that models asynchronous streams with Observables and operators like map, filter and switchMap. To avoid memory leaks, unsubscribe when the component is destroyed. The best options are the async pipe, takeUntilDestroyed(), take(1) for single-value streams, or storing the Subscription and calling unsubscribe() in ngOnDestroy.
private destroyRef = inject(DestroyRef);
ngOnInit() {
this.service.data$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(d => this.data = d);
}
20. What is the difference between switchMap, mergeMap, concatMap and exhaustMap?
All four map each value to an inner Observable but differ in how they handle overlap. switchMap cancels the previous inner request and keeps the latest, ideal for search typeahead. mergeMap runs all inner requests in parallel. concatMap queues them and runs one at a time in order. exhaustMap ignores new values while one is still running, useful for preventing double form submissions.
21. How does routing work in Angular and how do you configure routes?
The Angular Router maps URLs to components, so navigating changes the view without a full page reload. You define a Routes array with path and component entries, register it with provideRouter(routes), and render matched components in a <router-outlet>. Navigate with the routerLink directive or the Router service. Routes support parameters, child routes, redirects and wildcards.
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users/:id', component: UserComponent },
{ path: '**', component: NotFoundComponent },
];
22. What is lazy loading in Angular and how do you implement it?
Lazy loading delays loading a feature's code until the user navigates to its route, shrinking the initial bundle and speeding up startup. With standalone components you use loadComponent or loadChildren with a dynamic import(), and legacy apps lazy load an NgModule the same way. Combine it with preloading strategies like PreloadAllModules if needed. Also, @defer blocks lazy load parts of a template.
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
}
23. What are route guards in Angular?
Route guards control whether navigation to or from a route is allowed, such as for authentication or unsaved-changes checks. The types are canActivate, canActivateChild, canDeactivate, canMatch and resolve for pre-fetching data. In modern Angular guards are plain functions that use inject(), and class-based guards are deprecated. A guard returns a boolean, a UrlTree for redirecting, or an Observable or Promise of one.
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
return auth.isLoggedIn() || inject(Router).createUrlTree(['/login']);
};
24. What is the difference between template-driven and reactive forms?
Template-driven forms are defined mostly in the HTML template with ngModel and are simple, suited to basic forms. Reactive forms are built in the component class with FormGroup, FormControl and FormBuilder, giving explicit, synchronous control, easier unit testing, dynamic fields and custom validators. Reactive forms are preferred for complex or scalable forms, and need ReactiveFormsModule imported.
form = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
password: new FormControl('', Validators.minLength(8)),
});
25. What are HTTP interceptors and how do you use HttpClient?
HttpClient is Angular's service for making HTTP requests that return Observables, enabled with provideHttpClient(). Interceptors sit in the request pipeline and can modify outgoing requests or incoming responses, commonly to attach auth tokens, log, cache or handle errors globally. Modern interceptors are functions registered with withInterceptors().
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).token();
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};
26. What is the difference between signal(), computed() and effect(), and when should you avoid effect()?
signal() holds writable state, computed() derives a read-only, lazily evaluated and memoized value from other signals, and effect() runs a side effect whenever the signals it reads change. Avoid effect() for propagating state, such as setting another signal inside it, because that creates hidden data flow; use computed() or linkedSignal() instead. Reserve effects for syncing with the outside world, like logging, localStorage or non-Angular libraries.
first = signal('Ada');
last = signal('Lovelace');
full = computed(() => `${this.first()} ${this.last()}`);
constructor() {
effect(() => localStorage.setItem('name', this.full()));
}
27. What is the difference between signal inputs and the @Input decorator?
Signal inputs, declared with input(), expose the value as a read-only signal, while @Input() is a plain decorated property. Signal inputs can be required with input.required<T>(), work directly inside computed() and effect(), and are type-checked more strictly, so you no longer need ngOnChanges just to react to changes. Use model() when the child must also write the value back to the parent.
name = input.required<string>();
label = computed(() => this.name().toUpperCase());
28. What does the async pipe do and why is it better than subscribing manually?
The async pipe subscribes to an Observable or Promise in the template, renders the latest value and unsubscribes automatically when the component is destroyed. This prevents memory leaks, removes manual subscribe boilerplate and marks the view for check, so it works well with OnPush. A common pitfall is using it several times on the same Observable, which creates multiple subscriptions and multiple HTTP calls. Use @if (data$ | async; as data) or shareReplay to avoid that.
@if (user$ | async; as user) {
<p>{{ user.name }}</p>
}
29. Why is a function call in a template expression a performance problem, and how do you fix it?
A method call in a template, like {{ getTotal() }}, runs on every change detection cycle, so an expensive function can slow the whole view. Replace it with a computed() signal, a pure pipe, or a value calculated once when the data changes. Trivial getters are usually fine, but anything that loops, sorts or filters should be memoized. Signals and pure pipes only re-evaluate when their inputs change.
30. What is the trackBy function, or the track expression in @for, and why is it required?
Tracking tells Angular how to identify each item in a list so it can reuse existing DOM nodes when the array changes instead of destroying and recreating them. Without a stable identity, replacing an array with fresh objects re-renders every row and loses state such as focus or input text. The @for block makes track mandatory, and you should track by a unique id rather than by $index for lists that reorder.
@for (item of items(); track item.id) {
<li>{{ item.name }}</li>
} @empty {
<li>No items</li>
}
31. What is wrong with mutating an array in place when a component uses OnPush?
With OnPush, Angular compares input references, so this.items.push(x) keeps the same reference and the child view will not update. The fix is to create a new reference, for example this.items = [...this.items, x], or to hold the list in a signal and call update() with a new array. Immutable updates also make change tracking and debugging easier.
this.items.push(newItem);
this.items = [...this.items, newItem];
32. What is the ExpressionChangedAfterItHasBeenCheckedError and how do you fix it?
This development-mode error means a binding's value changed after Angular had already checked it in the same change detection pass, usually because a child or lifecycle hook like ngAfterViewInit modifies a value the parent template has already rendered. The real fix is to restructure so data flows downward, for example by computing the value earlier or using signals. Wrapping the change in setTimeout or calling detectChanges() only hides the design problem.
33. How do ViewChild and ContentChild differ, and when is the result available?
@ViewChild queries elements in the component's own template, while @ContentChild queries projected content passed between the component's tags with ng-content. View queries are resolved before ngAfterViewInit and content queries before ngAfterContentInit, so they are undefined in the constructor and usually in ngOnInit. The signal versions viewChild() and contentChild() return signals, which avoids the timing problem.
canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas');
ngAfterViewInit() { this.canvas().nativeElement.getContext('2d'); }
34. What is content projection and how does multi-slot projection work?
Content projection lets a parent pass markup into a component, which renders it where <ng-content> appears, making wrappers like cards and modals reusable. For multiple slots, give each ng-content a select attribute using a CSS selector, and the parent's elements are routed to the matching slot. Anything unmatched goes to the default slot with no select. Projected content is created by the parent, not the child.
<header><ng-content select="[card-title]"></ng-content></header>
<ng-content></ng-content>
35. What is the difference between ng-template, ng-container and ng-content?
ng-template defines a block of markup that is not rendered until you instantiate it, for example with *ngIf else branches or ngTemplateOutlet. ng-container is a grouping element that adds no node to the DOM, useful for applying a directive without an extra wrapper. ng-content is a placeholder that marks where projected content from the parent is inserted. They solve three different problems despite the similar names.
36. How do @defer blocks work and what are the available triggers?
@defer lazy loads a part of a template and its dependencies into a separate chunk, shrinking the initial bundle. The block renders after a trigger such as on viewport, on idle, on interaction, on hover, on timer(2s) or a when condition. You can add @placeholder, @loading and @error blocks, and prefetch triggers to download the code earlier. Only standalone components, directives and pipes used solely inside the block can be deferred.
@defer (on viewport) {
<app-comments />
} @placeholder {
<p>Comments will load here</p>
}
37. How would you share state between unrelated components?
Put the state in an injectable service and have each component inject it. A simple approach is a root-provided service exposing signals or a BehaviorSubject, with methods to update it. For larger apps with complex flows, a store library such as NgRx or NgRx SignalStore adds structure, devtools and predictable updates. Use @Input/@Output for parent-child communication and avoid global state when local state is enough.
@Injectable({ providedIn: 'root' })
export class CartService {
private _items = signal<Item[]>([]);
items = this._items.asReadonly();
add(i: Item) { this._items.update(l => [...l, i]); }
}
38. How do you implement a typeahead search with RxJS in Angular?
Combine a form control's valueChanges with debounceTime, distinctUntilChanged and switchMap. Debouncing limits requests while the user types, distinctUntilChanged skips repeated values and switchMap cancels the previous in-flight request so stale results never overwrite newer ones. Add filter to skip very short queries and catchError inside the inner pipe so one failure does not kill the stream.
results$ = this.search.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => this.api.find(q).pipe(catchError(() => of([]))))
);
39. What is the difference between Subject, BehaviorSubject and ReplaySubject?
A Subject multicasts values to current subscribers only, so late subscribers miss earlier emissions. A BehaviorSubject requires an initial value and gives every new subscriber the latest value immediately, which suits state. A ReplaySubject(n) replays the last n emissions to new subscribers. Expose them as read-only Observables with asObservable() so outside code cannot call next().
40. What is the difference between cold and hot Observables, and what does shareReplay do?
A cold Observable, like HttpClient.get, starts a new execution for every subscriber, while a hot one, like DOM events or a Subject, shares one source regardless of subscribers. Two subscribers to a cold HTTP Observable send two requests. shareReplay({ bufferSize: 1, refCount: true }) multicasts one execution and replays the last value to late subscribers, which is a common way to cache a request.
41. How do you handle errors from HttpClient properly?
Use the catchError operator to handle failures, then either return a fallback with of() or rethrow with throwError. For consistent handling, put cross-cutting logic such as redirecting on 401 or showing a toast in an interceptor, and keep component-specific recovery in the component. Use retry with a delay for transient errors, and avoid swallowing errors silently. A failing HTTP Observable completes the stream, so catch errors inside switchMap, not outside.
this.http.get<User[]>('/api/users').pipe(
retry({ count: 2, delay: 1000 }),
catchError(err => { this.log(err); return of([]); })
);
42. How do you write a custom validator for reactive forms, including a cross-field one?
A validator is a function that receives an AbstractControl and returns null when valid or an error object when invalid. For a cross-field check such as matching passwords, attach the validator to the parent FormGroup so it can read both controls. Async validators return an Observable or Promise and are passed as the third argument, running only after sync validators pass.
const match: ValidatorFn = (g) =>
g.get('pw')?.value === g.get('confirm')?.value ? null : { mismatch: true };
form = new FormGroup({ pw: new FormControl(''), confirm: new FormControl('') },
{ validators: match });
43. What is a ControlValueAccessor and when do you need one?
ControlValueAccessor is the interface that connects a custom component to Angular forms, so it works with formControlName or ngModel like a native input. You implement writeValue, registerOnChange, registerOnTouched and optionally setDisabledState, then provide the class with the NG_VALUE_ACCESSOR token. Use it for custom widgets such as star ratings, date pickers or rich text editors.
44. What is the difference between canActivate and canMatch, and what does a resolver do?
canActivate runs after a route matches and decides whether to enter it, while canMatch runs during matching and, if it returns false, lets the router try other routes with the same path and also prevents the lazy chunk from loading. That makes canMatch better for role-based route variants. A resolve function fetches data before the route activates, making it available through ActivatedRoute or withComponentInputBinding().
45. How can a component read route parameters, and how do you handle changing params on the same component?
Inject ActivatedRoute and use the paramMap Observable, or enable withComponentInputBinding() so route params arrive as component inputs. Reading snapshot.paramMap only gives the initial value, because Angular reuses the component when only the param changes, for example from /users/1 to /users/2, and ngOnInit does not run again. Subscribe to paramMap with switchMap to reload data.
user$ = inject(ActivatedRoute).paramMap.pipe(
map(p => p.get('id')!),
switchMap(id => this.api.getUser(id))
);
46. How do you optimize the performance of a large Angular application?
Start by measuring with Angular DevTools and the bundle analyzer, then apply the biggest wins. Use OnPush or signals, lazy load routes and use @defer, track @for lists, avoid heavy template functions, and prefer pure pipes. Reduce bundle size with tree-shaking, NgOptimizedImage, and by removing unused libraries. For huge lists use virtual scrolling from the CDK, and consider SSR with hydration to improve first paint.
47. What is SSR with hydration in Angular and what pitfalls should you watch for?
Server-side rendering produces HTML on the server for faster first paint and better SEO, and hydration lets the browser reuse that DOM instead of destroying and rebuilding it. You enable it with provideClientHydration(). Pitfalls include accessing window, document or localStorage directly, which do not exist on the server, direct DOM manipulation that breaks hydration matching, and code that renders differently on server and client. Guard browser-only code with afterNextRender or isPlatformBrowser.
48. How do you unit test a component that depends on a service and an HTTP call?
Configure TestBed with the component and provide a fake for the service, or use provideHttpClientTesting() with HttpTestingController to intercept requests. You then flush a mock response, call fixture.detectChanges() and assert on the DOM or output. Prefer testing behavior through the template over testing private methods, and call httpMock.verify() to make sure no unexpected requests remain.
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()],
});
const http = TestBed.inject(HttpTestingController);
http.expectOne('/api/users').flush([{ id: 1 }]);
49. How does Angular protect against XSS, and when is bypassSecurityTrustHtml dangerous?
Angular treats all values as untrusted by default and sanitizes or escapes them when they are bound into HTML, styles or URLs, so {{ }} and [innerHTML] cannot execute injected scripts. The DomSanitizer.bypassSecurityTrust* methods turn this protection off for a specific value, so using them on user-supplied content opens an XSS hole. Only bypass for content you fully control, and never build templates or URLs from raw user input.
50. What is the difference between providers and viewProviders, and how do the inject() flags like self and optional work?
providers makes a service visible to a component, its view and its projected content, while viewProviders limits visibility to the component's own view only, hiding it from projected content. When injecting, optional: true returns null instead of throwing if no provider exists, self looks only at the current element's injector, skipSelf starts at the parent, and host stops at the host element. They give precise control over hierarchical DI.
private parent = inject(ParentService, { optional: true, skipSelf: true });