Angular Roadmap
Angular is a complete, opinionated framework for building web applications in TypeScript. It includes routing, forms, an HTTP client, dependency injection, testing tools and server rendering, and its consistent structure makes it a common choice for large enterprise apps, internal tools and teams that value long-term maintainability. Angular interviews test whether you can build features cleanly and explain how the framework works: dependency injection, change detection, signals and RxJS.
This roadmap teaches modern Angular, with standalone components, built-in control flow, signals and zoneless change detection, while showing how to read the older styles you will meet in existing code. It moves from the CLI and templates through component communication, services, signals, RxJS, routing, forms and testing to state management, security, performance, server rendering and internationalisation. Each topic explains the idea, shows a short example and gives an exercise. Core topics are expected of every Angular developer, recommended ones appear in most jobs, and optional ones can wait.
By the end you will be able to build, test and deploy a production-quality Angular app and talk through your design choices with confidence. Practise in coding practice and track your progress in progress page.
- Level
- Beginner to advanced
- Time
- 12–16 weeks at about 8 hours a week, less if you already know TypeScript and another framework
- Scope
- 65 topics in 10 stages
- Before you start
- Modern JavaScript Roadmap: classes, modules, array methods, promises and
async/await; TypeScript Roadmap basics: types, interfaces, generics and classes with access modifiers; Basic HTML Roadmap and CSS Roadmap: semantic elements, forms, selectors and Flexbox; Comfort with npm, the terminal and Git and GitHub Roadmap
What you will be able to do
- Build modern Angular apps with standalone components, control flow blocks, signals and the CLI.
- Explain dependency injection, provider scopes and the injector hierarchy.
- Explain change detection, OnPush, zones and zoneless apps, and use signals effectively.
- Use RxJS operators confidently for HTTP, search, polling and combining streams.
- Build routed apps with guards, lazy loading, interceptors and typed reactive forms.
- Test services, components, directives, pipes and HTTP code, and write end-to-end tests.
- Ship secure, accessible, fast apps with server rendering and internationalisation where needed.
The roadmap at a glance
- Stage 1Foundations: Angular and Its ToolingBeginner · 1 week · 5 topics
- Stage 2Components and Template SyntaxBeginner · 2 weeks · 8 topics
- Stage 3Component Communication, Directives and PipesIntermediate · 1–2 weeks · 6 topics
- Stage 4Services, Dependency Injection and SignalsIntermediate · 2 weeks · 7 topics
- Stage 5RxJS and Reactive ProgrammingIntermediate · 2 weeks · 6 topics
- Stage 6Routing and HTTPIntermediate · 1–2 weeks · 6 topics
- Stage 7FormsIntermediate · 1–2 weeks · 6 topics
- Stage 8TestingIntermediate · 1–2 weeks · 7 topics
- Stage 9State Management, Libraries and BuildsAdvanced · 1–2 weeks · 5 topics
- Stage 10Security, Performance, SSR and InternationalisationAdvanced · 2–3 weeks · 9 topics
Stage 1 Foundations: Angular and Its Tooling
Goal. Create and run an Angular project with the CLI and understand how an Angular app is put together.
Introduction to Angular Core
Angular is a complete front-end framework maintained by Google. Where some libraries only render views, Angular ships routing, forms, an HTTP client, dependency injection, testing tools, server rendering and internationalisation in one package with one release schedule. It is written in and used with TypeScript Roadmap.
Angular and history: the original framework, AngularJS (version 1.x), was a different JavaScript framework. Angular 2 was a full rewrite, and since then the product has simply been called Angular, with major versions roughly every six months. AngularJS reached end of support in early 2022, so ignore tutorials that use $scope or ng-controller.
Practice. Write a short comparison of what Angular includes out of the box versus a library such as React.
Setting up a New Project Core
Local setup needs an active LTS version of Node.js and the Angular CLI, the command-line tool that creates, builds, tests and updates Angular projects.
npm install -g @angular/cli
ng new shop --style=scss
cd shop
ng serve --open # dev server with live reload
ng generate component product-list
ng build # production build in dist/
ng update # guided upgrades between versionsng new creates a workspace with an app, TypeScript and test configuration. ng generate (or ng g) scaffolds components, services, guards, pipes and more with the right files and imports. ng update runs migrations that rewrite code for new versions, which is one of Angular's strengths in large organisations.
The build uses esbuild with a Vite-based dev server in current versions.
Practice. Create a new project, generate two components and a service with the CLI, and read every generated file.
Angular Architecture Core
Angular architecture is built from a few kinds of building blocks.
- Components own a piece of the screen: a TypeScript class, a template and styles.
- Directives add behaviour to existing elements.
- Pipes transform values for display.
- Services hold logic and state that is not tied to one view, such as API access.
- Dependency injection creates services and hands them to whoever asks.
- The router maps URLs to components.
An app starts from main.ts, which calls bootstrapApplication(AppComponent, appConfig). The app.config.ts file lists app-wide providers such as the router, HTTP client and hydration. From there the component tree renders, and change detection keeps the DOM in sync with data.
Practice. Draw the architecture of a small shopping app as components, services and routes before writing code.
Standalone Components and Modules Core
Standalone components declare their own dependencies in an imports array and need no NgModule. They have been the default since Angular 19, and new code should use them.
Before that, every component belonged to an NgModule, a class with declarations, imports, exports and providers. You will still meet modules in existing apps: feature modules group one area of an app, shared modules re-export common pieces, and module architecture questions come up when maintaining older code.
@NgModule({
declarations: [OrdersListComponent],
imports: [CommonModule, RouterModule.forChild(routes)],
})
export class OrdersModule {}Creating modules is done with ng generate module. The CLI can migrate a module-based app to standalone with ng generate @angular/core:standalone. Libraries may still export modules, which standalone components can import directly.
Practice. Convert a small module-based app to standalone components using the CLI migration and compare the code.
Developer Tools Recommended
Three developer tools make Angular work faster.
- Angular DevTools, a browser extension, shows the component tree with inputs, outputs and state, the injector hierarchy, signal graphs and a profiler that records change detection cycles.
- The Angular Language Service, built into the official VS Code extension, gives template autocompletion, type errors in templates and go-to-definition from HTML to TypeScript.
- The CLI itself:
ng lintafter adding ESLint,ng test,ng e2eandng build --stats-jsonfor bundle analysis.
Turn on strictTemplates in tsconfig.json so the compiler type-checks templates as strictly as TypeScript code. It catches wrong property names and type mismatches in bindings at build time.
Practice. Install Angular DevTools, profile a click that updates a list, and find which components were checked.
Stage 2 Components and Template Syntax
Goal. Build components with templates, bindings, control flow and lifecycle hooks.
Component Anatomy Core
Component anatomy: a class decorated with @Component, whose metadata tells Angular how to create and render it.
@Component({
selector: 'app-product-card',
imports: [CurrencyPipe, RouterLink],
template: `
<h3>{{ product().name }}</h3>
<p>{{ product().price | currency:'GBP' }}</p>
`,
styles: `h3 { margin: 0; }`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductCardComponent {
product = input.required<Product>();
}- The selector is the custom element name used in other templates.
- The template is inline or in a separate file with
templateUrl. - Styles are inline or in
styleUrl. - Imports list the components, directives and pipes the template uses.
The class holds state and methods the template can use.
Practice. Write a profile card component from scratch without the CLI, then compare it with a generated one.
Creating Components Core
Creating components with ng generate component name produces the class, template, styles and a test file, with sensible options such as inline templates or skipped tests.
Good component design:
- Keep components small and focused on one job.
- Split presentational components, which only receive inputs and emit outputs, from containers, also called smart components, which inject services and fetch data.
- Use
OnPushchange detection by default. - Keep logic out of templates; put it in the class or a service.
ng g c features/cart/cart-summary --inline-style --skip-testsThe container and presentational split makes testing easier, because most components need no services at all.
Practice. Build a product page as one container component and three presentational children.
Encapsulation and Styles Recommended
By default, Angular uses emulated view encapsulation: it adds generated attributes to a component's elements and rewrites its CSS selectors, so styles apply only to that component.
The encapsulation option changes this:
Emulated, the default, scopes styles as above.ShadowDomuses the browser's real Shadow DOM.Nonemakes the styles global, which is occasionally useful for theming.
Special selectors help: :host targets the component's own element and :host-context() reacts to ancestors. The old ::ng-deep pierces encapsulation but is deprecated; prefer CSS custom properties to let parents theme children.
App-wide styles, such as resets and design tokens, live in styles.scss.
Practice. Theme a button component through CSS custom properties set by its parent instead of ::ng-deep.
Data Binding Core
Data binding connects the class and the template. Angular templates support several forms of binding.
- Interpolation:
{{ user.name }}inserts text, escaped for safety. - Property binding:
[src]="imageUrl"sets a DOM property or a component input. - Attribute binding:
[attr.aria-label]="label"sets an HTML attribute that has no DOM property, such as ARIA attributes orcolspan. - Class and style bindings:
[class.active]="isActive",[style.width.px]="w".
<img [src]="product.image" [alt]="product.name" />
<button [disabled]="saving()" [attr.aria-busy]="saving()">Save</button>
<li [class.selected]="item.id === selectedId()">{{ item.name }}</li>Properties and attributes are different things: binding the DOM property is the usual choice, and attributes are only needed when no property exists.
Practice. Build a progress bar component that uses property, attribute, class and style bindings together.
Event Binding and Template Statements Core
Event binding listens to DOM or component events with parentheses: (click)="save()". The code on the right is a template statement, which can call methods, assign values and use $event, the event object.
<button (click)="addToCart(product)">Add</button>
<input (keyup.enter)="search(query.value)" #query />
<app-rating (rated)="onRated($event)" />Key filters such as keyup.enter or keydown.shift.tab are built in. Template statements are deliberately limited: no new, no increment operators in older versions, and no access to globals such as window. Keep them to one method call.
Reference vars, declared with #name, point at an element or component in the template, so #query above gives direct access to the input element without a class property.
Practice. Build a search box that searches on Enter, clears on Escape and focuses through a template reference variable.
Two-way Binding Core
Two-way binding, written [(value)] and nicknamed banana-in-a-box, combines a property binding and an event binding so a value flows in both directions.
With forms, [(ngModel)] from FormsModule binds an input to a property. With your own components, a model() input creates a two-way bindable property:
@Component({ selector: 'app-counter', template: `
<button (click)="value.set(value() - 1)">-</button> {{ value() }}
<button (click)="value.set(value() + 1)">+</button>` })
export class CounterComponent {
value = model(0);
}
<!-- parent -->
<app-counter [(value)]="quantity" />Under the hood, [(value)]="x" means [value]="x" plus (valueChange)="x = $event". Use two-way binding for form-like components; one-way data flow is easier to follow elsewhere.
Practice. Build a toggle switch component that supports two-way binding with model().
Control Flow Core
Built-in control flow blocks, introduced in Angular 17, replace the old *ngIf, *ngFor and ngSwitch directives.
@if (user(); as u) {
<p>Welcome, {{ u.name }}</p>
} @else if (loading()) {
<p>Loading...</p>
} @else {
<a routerLink="/login">Sign in</a>
}
@for (item of items(); track item.id; let i = $index) {
<li>{{ i + 1 }}. {{ item.name }}</li>
} @empty { <li>No items yet.</li> }
@switch (status()) { @case ('paid') { Paid } @default { Pending } }@for requires a track expression, which identifies items so the DOM is reused correctly. @let declares a local template variable, such as @let total = subtotal() + tax();. Run ng generate @angular/core:control-flow to migrate old directives.
Practice. Rewrite a template that uses *ngIf, *ngFor and ngSwitch with the built-in control flow blocks.
Lifecycle hooks Core
Lifecycle hooks are methods Angular calls at points in a component's life. Implement the matching interface to document them.
ngOnInit: after the first inputs are set; a common place for initial work.ngOnChanges: whenever an input changes, with previous and current values.ngAfterContentInitandngAfterViewInit: after projected content or the component's own view is ready, so content and view queries are available.ngOnDestroy: clean up timers, listeners and subscriptions.ngDoCheck: custom change detection; rarely needed.
Newer APIs reduce the need for hooks. Signal inputs with computed replace most ngOnChanges logic, afterNextRender and afterEveryRender run DOM work after rendering, and DestroyRef with takeUntilDestroyed handles clean-up without implementing ngOnDestroy.
Practice. Log every lifecycle hook in a component and change its input from the parent to see the order.
Stage 3 Component Communication, Directives and Pipes
Goal. Pass data between components, project content, write directives and transform values with pipes.
Component Communication Core
Component communication follows parent-child interaction rules: parents pass data down through inputs, and children send events up through outputs.
export class ProductCardComponent {
product = input.required<Product>();
compact = input(false);
addToCart = output<Product>();
}
<!-- parent -->
<app-product-card [product]="p" (addToCart)="cart.add($event)" />input() and output() are the modern function-based APIs. The older @Input() and @Output() decorators with EventEmitter do the same job and appear in most existing code, so learn to read both styles of input output declarations.
For siblings or distant components, share a service that holds state in signals or observables. Avoid long chains of inputs and outputs passing data through components that do not use it.
Practice. Build a list and detail pair where selecting an item in the list shows it in a sibling panel through a shared service.
ViewChild, ContentChild and Content Projection Recommended
Content projection lets a parent pass markup into a component with <ng-content>, which is how cards, dialogs and layout components work. select targets named slots.
<!-- card.component.html -->
<header><ng-content select="[card-title]" /></header>
<ng-content />Queries give a component a reference to elements or components:
- ViewChild finds something in the component's own template.
- ContentChild finds something in projected content.
viewChildrenandcontentChildrenreturn lists.
nameInput = viewChild<ElementRef<HTMLInputElement>>('name');
tabs = contentChildren(TabComponent);
focus() { this.nameInput()?.nativeElement.focus(); }These are queries as signals, the modern form; the decorators @ViewChild and @ContentChild are the older equivalent. Use queries for imperative actions such as focus, not to pass data that inputs could carry. Dynamic components, chosen at run time, are rendered with NgComponentOutlet or ViewContainerRef.createComponent, as dialog services do.
Practice. Build a tabs component that finds its projected tab children with contentChildren.
Attribute Directives Core
Directives add behaviour to elements. Attribute directives change the appearance or behaviour of an existing element without changing the DOM structure. NgClass and NgStyle are built in, though plain class and style bindings now cover most uses.
Custom directives are written like components without templates:
@Directive({ selector: '[appHighlight]', host: {
'(mouseenter)': 'on.set(true)',
'(mouseleave)': 'on.set(false)',
'[style.backgroundColor]': 'on() ? color() : null',
}})
export class HighlightDirective {
color = input('yellow', { alias: 'appHighlight' });
on = signal(false);
}
<p appHighlight="lightblue">Hover me</p>The host metadata binds events and properties on the host element; the @HostListener and @HostBinding decorators are the older way. Host directives let a component reuse directive behaviour through composition.
Practice. Write an appAutofocus directive and a appTooltip directive with a configurable message.
Structural Directives Recommended
Structural directives decide which elements exist at all, stamping templates into the page or taking them out. They are written with an asterisk, *ngIf, which is shorthand for wrapping the element in an <ng-template>.
Built-in control flow blocks have replaced *ngIf, *ngFor and *ngSwitch for new code, but you can still write your own structural directives:
@Directive({ selector: '[appIfRole]' })
export class IfRoleDirective {
private tpl = inject(TemplateRef);
private vcr = inject(ViewContainerRef);
private auth = inject(AuthService);
appIfRole = input.required<string>();
constructor() {
effect(() => {
this.vcr.clear();
if (this.auth.hasRole(this.appIfRole())) this.vcr.createEmbeddedView(this.tpl);
});
}
}TemplateRef is the template to stamp out and ViewContainerRef is where to put it.
Practice. Write an *appRepeat="3" structural directive that renders its template a given number of times.
Builtin pipes Core
Pipes transform values in templates with the | operator. Builtin pipes cover the common pipes most apps need:
date,currency,number,percent: locale-aware formatting.uppercase,lowercase,titlecase.jsonfor debugging andkeyvalueto loop over objects.slicefor arrays and strings.asyncsubscribes to an observable or promise and unsubscribes automatically.
{{ order.total | currency:'GBP' }}
{{ order.date | date:'d MMM y' }}
@for (entry of settings | keyvalue; track entry.key) { ... }
@if (user$ | async; as user) { {{ user.name }} }Arguments follow a colon. Pipes are imported into a component like any other dependency. Pipes precedence: the pipe operator has lower precedence than most operators but higher than the ternary, so wrap expressions in brackets when in doubt, for example (a ? b : c) | uppercase.
Practice. Format a table of orders with date, currency, percent and titlecase pipes in the UK locale.
Custom and Impure Pipes Recommended
A custom pipe is a class with a transform method.
@Pipe({ name: 'initials' })
export class InitialsPipe implements PipeTransform {
transform(name: string): string {
return name.split(' ').map(p => p[0]).join('').toUpperCase();
}
}By default pipes are pure: Angular reruns them only when an input value or reference changes, which makes them cheap and an alternative to calling methods in templates.
Impure pipes, declared with pure: false, run on every change detection cycle, so they notice mutations inside arrays and objects. They can be slow, and the async pipe is the main built-in example. Usually it is better to create a new array when data changes, or use a computed signal, than to write an impure pipe.
Practice. Write a pure timeAgo pipe and explain why it does not update by itself as time passes.
Stage 4 Services, Dependency Injection and Signals
Goal. Share logic through injected services, manage state with signals and understand how change detection works.
Services Core
Services are classes that hold logic and state not tied to a single view: API calls, caching, authentication, logging and shared state. Components stay thin and delegate to services.
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
readonly count = computed(() => this.items().length);
add(item: CartItem) { this.items.update(list => [...list, item]); }
}
export class HeaderComponent {
cart = inject(CartService);
}providedIn: 'root' creates one shared instance for the app, and the service is tree-shaken if nothing uses it. Generate services with ng g service. Expose read-only state and change it only through methods, so updates are predictable.
Practice. Move all HTTP calls and state out of a large component into a service and inject it.
Dependency Injection Core
Dependency injection (DI) means a class asks for what it needs and Angular supplies it, instead of the class constructing its own dependencies. That makes code easier to test, because a test can provide a fake.
You request dependencies with the inject() function or through constructor parameters:
export class OrdersComponent {
private api = inject(OrdersApi);
constructor(private logger: Logger) {} // older style
}Angular resolves dependencies through a hierarchy of injectors: the environment injectors (root and lazy-loaded routes) and the element injectors created for components and directives. A lookup starts at the requesting component and walks up the tree until it finds a provider. Options such as optional, self and skipSelf change that search.
inject() only works in an injection context, such as a constructor or field initialiser.
Practice. Provide a fake logger in one component subtree and show that only that subtree receives it.
Providers Core
Providers tell an injector how to create a dependency. They can be listed in app.config.ts, on a route, or on a component.
providers: [
ApiService, // class
{ provide: Logger, useClass: ConsoleLogger }, // swap implementation
{ provide: API_URL, useValue: '/api' }, // value
{ provide: Clock, useFactory: () => new Clock(Date.now) },
{ provide: OldService, useExisting: NewService }, // alias
]
export const API_URL = new InjectionToken<string>('API_URL');InjectionToken is used for values and interfaces that have no class at run time. Component-level providers create a new instance for each component instance and its children. viewProviders does the same but hides the instance from projected content, which matters when building reusable components that accept ng-content.
Practice. Provide a different implementation of a storage service per environment using a factory provider.
Signals Core
Signals are Angular's reactive primitive. A signal holds a value and notifies whoever reads it when it changes, which lets Angular update exactly the views that depend on it.
count = signal(0);
double = computed(() => this.count() * 2);
increment() { this.count.update(c => c + 1); }
constructor() {
effect(() => console.log('count is', this.count()));
}You read a signal by calling it. set and update change writable signals. computed derives values lazily and caches them. effect runs side effects when its dependencies change; use it sparingly, for syncing with non-Angular code, not for deriving state. linkedSignal creates writable state that resets when a source changes, and resource loads async data into signals.
Signals pair well with OnPush and make zoneless apps possible.
Practice. Build a shopping cart with signals and computed totals, without RxJS or manual change detection.
Inputs as Signals and Model Inputs Core
Signal-based inputs make component inputs read-only signals, so derived values are just computed signals and ngOnChanges is rarely needed.
export class UserBadgeComponent {
user = input.required<User>();
size = input<'sm' | 'lg'>('sm');
label = computed(() => `${this.user().first} ${this.user().last}`);
open = model(false); // two-way: [(open)]
closed = output<void>();
}input.required fails at compile time if the parent forgets it. Options include alias and transform, for example booleanAttribute so <app-x disabled> works.
Model inputs, created with model(), are writable inside the component, and writing to them emits a change event the parent can bind with [(open)].
Queries also have signal forms: viewChild, viewChildren, contentChild and contentChildren. CLI migrations convert decorator inputs and queries to these APIs.
Practice. Convert a component with @Input, ngOnChanges and @ViewChild to signal inputs, computed and viewChild.
Change Detection Core
Change detection is how Angular keeps the DOM in sync with component data. When something may have changed, Angular checks the component tree, re-evaluates template bindings and updates the DOM where values differ.
Two strategies:
Default: every component is checked on every cycle.OnPush: a component is checked only when an input reference changes, an event fires inside it, anasyncpipe emits, or a signal it reads changes. Use it everywhere; it is much cheaper and forces immutable updates.
With OnPush, mutating an object passed as an input does not update the child; replace the object instead. In development, Angular runs a second check and throws ExpressionChangedAfterItHasBeenChecked if a binding changed during the check, which usually means state is being changed in a lifecycle hook.
Practice. Switch a component tree to OnPush, find what stops updating, and fix it with immutable updates or signals.
Zones and Zoneless Applications Recommended
Historically, Angular used Zone.js, a library that patches browser APIs such as setTimeout, promises and event listeners, to know when async work finished and trigger change detection automatically. Zones made Angular feel magical but add bundle size, slow down start-up and run change detection more often than needed.
Zone pollution is when third-party code, such as a chart library or a frequent mousemove listener, triggers needless cycles. The fix is to run it outside Angular's zone with NgZone.runOutsideAngular.
Zoneless applications drop Zone.js entirely. Change detection is triggered by signals, template events, async pipe emissions and markForCheck. Zoneless change detection is stable in current Angular and new projects use it by default. Enable it in older apps with provideZonelessChangeDetection(), after moving state to signals and OnPush.
Practice. Remove Zone.js from a small signal-based app and fix any view that no longer updates.
Stage 5 RxJS and Reactive Programming
Goal. Use observables and the most important RxJS operators confidently, and connect them to signals.
RxJS Basics Core
RxJS is a library for working with streams of values over time. Angular's HttpClient, router events and reactive forms all expose observables, so you need the basics even in signal-first apps.
The observable pattern: an observable produces values, an observer subscribes with next, error and complete callbacks, and the subscription can be cancelled.
const ticks$ = interval(1000).pipe(take(3));
const sub = ticks$.subscribe({
next: v => console.log(v),
complete: () => console.log('done'),
});
sub.unsubscribe();Observable lifecycle: nothing runs until someone subscribes; each subscriber normally gets its own execution (cold observables); the stream ends with complete or error, after which no more values arrive. Subjects are both observable and observer and multicast to several subscribers; BehaviorSubject holds a current value.
Practice. Create an observable from a button click and log the click positions until the user has clicked five times.
RxJS vs Promises Core
RxJS vs promises is a common interview question. The differences that matter:
- A promise represents one future value; an observable can emit zero, one or many values.
- A promise starts work immediately when created; an observable is lazy and starts on subscribe.
- A promise cannot be cancelled; unsubscribing from an observable cancels the work, for example aborting an HTTP request.
- Observables come with operators for retrying, debouncing, combining and switching streams.
Use firstValueFrom or lastValueFrom to turn an observable into a promise when you want await, and from(promise) for the reverse. For a single HTTP call, either works; for events, typing, polling and WebSockets, observables are a better fit.
Practice. Implement the same autocomplete once with promises and once with observables, and compare cancellation.
Transformation and Filtering Operators Core
RxJS operators are functions you chain with pipe to build new streams.
Transformation operators change values: map reshapes each value, scan accumulates state like a running reduce, and the higher-order mapping operators turn each value into an inner observable:
switchMapcancels the previous inner stream: ideal for search and route params.mergeMapruns inner streams in parallel.concatMapqueues them in order: ideal for saves that must not overlap.exhaustMapignores new values while busy: ideal for a submit button.
Filtering operators let values through selectively: filter, take, first, skip, distinctUntilChanged and takeUntil.
this.route.paramMap.pipe(
map(p => p.get('id')!),
distinctUntilChanged(),
switchMap(id => this.api.getProduct(id)),
);
Practice. Explain with a diagram what happens to an in-flight request with each of the four higher-order mapping operators.
Combination and Rate Limiting Operators Recommended
Combination operators merge several streams:
combineLatestemits the latest value of each once all have emitted; good for filters plus data.forkJoinwaits for all to complete and emits once, likePromise.all.mergeinterleaves values from several sources.withLatestFromsamples another stream when the main one emits.
Rate limiting operators control how often values pass:
debounceTimewaits for a pause, as in search boxes.throttleTimelets one value through per time window, as in scroll handlers.auditTimeandsampleTimeemit the most recent value at intervals.
fromEvent(input, 'input').pipe(
map(e => (e.target as HTMLInputElement).value),
debounceTime(300),
distinctUntilChanged(),
);
Practice. Build a product filter that combines a debounced search term, a category select and a sort order.
Complex Sequences and Error Handling Recommended
Complex sequences combine the operators above, and they need careful error handling, because an error ends a stream permanently.
search$ = this.query$.pipe(
debounceTime(300),
switchMap(q => this.api.search(q).pipe(
retry({ count: 2, delay: 500 }),
catchError(() => of([])), // inner catch keeps the outer stream alive
)),
shareReplay({ bufferSize: 1, refCount: true }),
);Put catchError inside the inner observable so one failed request does not kill the search box. shareReplay shares one execution among several subscribers and replays the last value, which avoids duplicate HTTP calls.
Avoid nesting subscribe calls inside each other; use a higher-order operator. Always end subscriptions with the async pipe, takeUntilDestroyed() or toSignal, or they leak.
Practice. Build a polling stream that refreshes every 10 seconds, retries failures and pauses while the tab is hidden.
RxJS Interop Core
RxJS interop utilities in @angular/core/rxjs-interop connect observables and signals, so you can use each where it fits.
// observable to signal: subscribes now, unsubscribes on destroy
products = toSignal(this.api.getProducts(), { initialValue: [] });
// signal to observable: for debouncing, switching and other operators
results = toSignal(
toObservable(this.query).pipe(
debounceTime(300),
switchMap(q => this.api.search(q)),
),
{ initialValue: [] },
);
// automatic clean-up of a manual subscription
this.socket.messages$.pipe(takeUntilDestroyed()).subscribe(m => this.add(m));A useful rule: signals for state shown in templates, RxJS for events and async workflows over time. rxResource loads data from an observable into a resource signal.
Practice. Refactor a component that uses manual subscriptions and ngOnDestroy to use toSignal and takeUntilDestroyed.
Stage 6 Routing and HTTP
Goal. Build multi-page apps with the router, protect routes, and talk to APIs with HttpClient and interceptors.
Routing Core
Routing maps URLs to components. Routes are an array passed to provideRouter in app.config.ts.
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'products/:id', component: ProductComponent },
{ path: 'admin', loadChildren: () => import('./admin/routes') },
{ path: '**', component: NotFoundComponent },
];
providers: [provideRouter(routes, withComponentInputBinding())]Router outlets, <router-outlet />, mark where the matched component renders; nested routes render inside a parent's outlet. Router links, <a routerLink="/products/42" routerLinkActive="active">, navigate without reloading. In code, inject Router and call navigate.
withComponentInputBinding() passes route params and query params straight into component inputs, so id = input<string>() receives :id.
Practice. Build a catalogue with a list route, a detail route bound to an input, nested tabs and a 404 page.
Guards and Resolvers Core
Guards decide whether navigation can continue. Modern guards are plain functions.
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
return auth.isLoggedIn()
? true
: inject(Router).createUrlTree(['/login'], { queryParams: { next: state.url } });
};
{ path: 'account', component: AccountComponent, canActivate: [authGuard] }Types include canActivate, canActivateChild, canDeactivate (for unsaved changes), and canMatch (to choose between routes or block lazy loading). Class-based guards are deprecated.
Resolvers load data before a route activates, so the page never renders empty. They delay navigation, though, so many teams prefer rendering immediately with a loading state. Client-side guards improve experience; the API must still enforce access.
Practice. Add an auth guard with redirect, a role-based canMatch guard and an unsaved-changes guard to an app.
Lazy loading Core
Lazy loading splits the app so code for a route is downloaded only when the user first visits it, which shrinks the initial bundle.
{ path: 'reports', loadComponent: () => import('./reports/reports.component')
.then(m => m.ReportsComponent) },
{ path: 'admin', loadChildren: () => import('./admin/admin.routes') },loadComponent lazy-loads one standalone component; loadChildren lazy-loads a whole set of child routes. In older apps, lazy loading modules worked the same way with loadChildren returning an NgModule.
Preloading strategies download lazy chunks in the background after start-up: withPreloading(PreloadAllModules) preloads everything, and custom strategies can preload only likely routes. Within a page, @defer blocks give the same benefit for components.
Practice. Lazy-load two feature areas, check the network tab to confirm separate chunks, then add a preloading strategy.
Router Events and Route Transitions Optional
Router events are an observable stream on the Router describing each navigation: NavigationStart, GuardsCheckEnd, ResolveEnd, NavigationEnd, NavigationCancel and NavigationError.
inject(Router).events.pipe(
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
takeUntilDestroyed(),
).subscribe(e => analytics.page(e.urlAfterRedirects));Common uses are page-view tracking, a global loading bar and restoring focus for screen-reader users after navigation.
Route transitions animate between pages. The modern approach is the browser View Transitions API, enabled with withViewTransitions() in provideRouter, which cross-fades pages with CSS. The older approach used the animations package with a trigger on the router outlet.
Practice. Add a top loading bar driven by router events and enable view transitions between pages.
HTTP Client Core
Setting up the client: add provideHttpClient() to app.config.ts, optionally withFetch() to use the Fetch API, which suits server rendering.
Making requests with HttpClient returns cold observables, so nothing is sent until you subscribe.
@Injectable({ providedIn: 'root' })
export class ProductsApi {
private http = inject(HttpClient);
list(page = 1) {
return this.http.get<Product[]>('/api/products', { params: { page } });
}
create(p: NewProduct) { return this.http.post<Product>('/api/products', p); }
}The generic type is an assertion, not validation. Errors arrive as HttpErrorResponse. Keep HTTP calls in services, expose them to templates with toSignal or the async pipe, and cancel stale requests with switchMap. httpResource is a newer signal-based way to fetch data reactively.
Practice. Write a typed API service with list, get, create and delete methods and display results with error handling.
Writing Interceptors Core
Interceptors sit between your code and the network, so cross-cutting concerns live in one place: adding auth headers, logging, retries, showing a loading indicator and mapping errors.
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).token();
const authReq = token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req;
return next(authReq).pipe(
catchError(err => {
if (err.status === 401) inject(Router).navigate(['/login']);
return throwError(() => err);
}),
);
};
provideHttpClient(withInterceptors([authInterceptor, loggingInterceptor]))Requests are immutable, so you clone them to change them. Interceptors run in the order listed for requests and in reverse for responses. Functional interceptors are preferred over class-based ones.
Practice. Write interceptors that add a token, retry idempotent GET requests once, and log request durations.
Stage 7 Forms
Goal. Build template-driven and reactive forms with typed models, custom validation and reusable form controls.
Forms in Angular Core
Angular has two built-in forms approaches, both of which track value, validity, touched and dirty state for each control.
- Template-driven forms are defined mostly in the template with
ngModel. They suit small, simple forms. - Reactive forms are defined in the component class as a tree of
FormControl,FormGroupandFormArrayobjects. They are explicit, typed, easy to test and suit complex or dynamic forms.
Both add CSS classes such as ng-invalid and ng-touched for styling, and both support synchronous and asynchronous validators.
Most professional codebases use reactive forms. A newer signal-based forms API is being developed; check its status before using it in production. Whichever approach you use, show errors only after a field is touched, link messages to inputs for accessibility and validate again on the server.
Practice. List the forms in an app you use and decide which approach each one should use and why.
Template-driven Forms Recommended
Template-driven forms come from FormsModule. You bind inputs with [(ngModel)] and add validation with HTML attributes such as required and minlength.
<form #f="ngForm" (ngSubmit)="save(f.value)">
<input name="email" [(ngModel)]="model.email" required email #email="ngModel" />
@if (email.invalid && email.touched) {
<p class="error">Enter a valid email address.</p>
}
<button [disabled]="f.invalid">Save</button>
</form>Every control needs a name. Template reference variables give access to the NgForm and NgModel directives and their state.
They are quick to write but harder to unit-test, harder to make dynamic, and their models are updated asynchronously, which can surprise you in tests.
Practice. Build a contact form with template-driven validation and error messages shown after touch.
Reactive Forms Core
Reactive forms come from ReactiveFormsModule. The form model lives in the class and the template binds to it.
private fb = inject(NonNullableFormBuilder);
form = this.fb.group({
name: ['', [Validators.required, Validators.maxLength(50)]],
email: ['', [Validators.required, Validators.email]],
addresses: this.fb.array([this.fb.control('')]),
});
<form [formGroup]="form" (ngSubmit)="submit()">
<input formControlName="name" />
<input formControlName="email" />
</form>valueChanges and statusChanges are observables, so you can react to edits with RxJS. setValue, patchValue and reset update the form from code. FormArray handles repeating groups such as phone numbers. Because the model is plain code, reactive forms are straightforward to unit-test without rendering.
Practice. Build a checkout form with nested address groups and a dynamic list of discount codes.
Typed Forms Core
Typed forms, available since Angular 14, infer the type of every control from its initial value, so form.value.email is string | undefined and typos fail to compile.
const login = new FormGroup({
email: new FormControl('', { nonNullable: true }),
remember: new FormControl(false, { nonNullable: true }),
});
login.getRawValue(); // { email: string; remember: boolean }form.value makes every field optional because disabled controls are excluded; getRawValue() includes them. nonNullable: true, or NonNullableFormBuilder, makes reset() return to the initial value rather than null.
UntypedFormGroup exists for migrated code. Define form types next to your API models so the form and the request body stay aligned.
Practice. Convert an untyped form to typed forms and fix every type error the compiler reports.
Custom Validators and Dynamic Forms Recommended
Custom validators are functions that receive a control and return null when valid or an error object when not.
export const noSpaces: ValidatorFn = c =>
/\s/.test(c.value ?? '') ? { noSpaces: true } : null;
export const matchPasswords: ValidatorFn = g =>
g.get('password')?.value === g.get('confirm')?.value ? null : { mismatch: true };Validators on a FormGroup compare fields. Async validators return an observable or promise, for example to check if a username is taken; debounce them and they run only after sync validators pass.
Dynamic forms are built at run time from configuration, such as a JSON description of questions in a survey. Loop over the configuration with @for, create controls in a FormGroup from the same configuration, and render the right input per type.
Practice. Build a survey form generated from a JSON definition, including a cross-field validator.
Control Value Accessor Optional
ControlValueAccessor is the interface that connects a custom component to Angular forms, so it works with formControlName and ngModel just like a native input. Use it for date pickers, rating widgets and rich selects.
@Component({
selector: 'app-rating',
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: RatingComponent, multi: true }],
template: `...`,
})
export class RatingComponent implements ControlValueAccessor {
value = 0; disabled = false;
private onChange = (v: number) => {}; private onTouched = () => {};
writeValue(v: number) { this.value = v ?? 0; }
registerOnChange(fn: (v: number) => void) { this.onChange = fn; }
registerOnTouched(fn: () => void) { this.onTouched = fn; }
setDisabledState(d: boolean) { this.disabled = d; }
select(v: number) { this.value = v; this.onChange(v); this.onTouched(); }
}
Practice. Build a star rating component that works with both reactive and template-driven forms.
Stage 8 Testing
Goal. Test services, components, directives, pipes and HTTP code, and write end-to-end tests for key journeys.
Testing in Angular Core
Angular projects come with testing configured. TestBed creates a testing module where you provide dependencies and render components.
Test runners have changed. Older projects use Jasmine with Karma, which has been deprecated. Current CLI versions use Vitest for new projects, and Jest is also widely used through community builders. The Angular testing utilities are the same whichever runner you use.
ng test # run unit tests
ng test --code-coverage # with a coverage reportWrite mostly unit and component tests, plus a few end-to-end tests. Test behaviour that users or callers see, not private methods. Consider Angular Testing Library, which provides user-centred queries like getByRole on top of TestBed.
Practice. Run the generated tests in a new project, break one on purpose and read the failure output.
Testing Services Core
Testing services is the simplest case: a service without dependencies can be created with new. When testing services with dependencies, use TestBed to provide fakes.
describe('CartService', () => {
let cart: CartService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{ provide: PriceApi, useValue: { taxRate: () => 0.2 } }],
});
cart = TestBed.inject(CartService);
});
it('adds tax to the total', () => {
cart.add({ id: '1', price: 100 });
expect(cart.total()).toBe(120);
});
});Fakes can be plain objects, spy objects or small hand-written classes. Prefer fakes that behave realistically to long chains of mocked return values.
Practice. Test a service that depends on an API service and a logger, faking both.
Testing Components Core
Component tests render the component with TestBed and interact with it through the DOM.
Testing component bindings checks that inputs are shown and outputs fire; testing component templates checks conditional and repeated content.
it('shows the product name and emits on click', async () => {
const fixture = TestBed.createComponent(ProductCardComponent);
fixture.componentRef.setInput('product', { id: '1', name: 'Lamp', price: 20 });
await fixture.whenStable();
const el: HTMLElement = fixture.nativeElement;
expect(el.textContent).toContain('Lamp');
let emitted: Product | undefined;
fixture.componentInstance.addToCart.subscribe(p => (emitted = p));
el.querySelector('button')!.click();
expect(emitted?.id).toBe('1');
});Use setInput for inputs, including signal inputs. With zoneless apps, wait with whenStable rather than relying on detectChanges.
Practice. Test a component for its empty, loading and populated states and one output event.
Testing Directives and Pipes Recommended
Testing pipes is easy: a pure pipe is a class with a transform method, so instantiate it and call it.
it('builds initials', () => {
expect(new InitialsPipe().transform('Ada Lovelace')).toBe('AL');
});Testing directives works best through a small host component that uses the directive in a template, because a directive's job is to change an element.
@Component({ imports: [HighlightDirective],
template: `<p appHighlight="yellow">Text</p>` })
class HostComponent {}Render the host, trigger the events the directive listens to, and check the element's styles or attributes. This tests the directive exactly as real templates use it.
Practice. Test the highlight directive through a host component, including a custom colour input.
Testing Requests Core
Testing requests uses provideHttpClientTesting(), which replaces the real back end with a controller that lets you inspect and answer requests.
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()],
});
api = TestBed.inject(ProductsApi);
http = TestBed.inject(HttpTestingController);
});
it('loads products', () => {
let result: Product[] = [];
api.list().subscribe(r => (result = r));
http.expectOne('/api/products?page=1').flush([{ id: '1', name: 'Lamp' }]);
expect(result.length).toBe(1);
});
afterEach(() => http.verify()); // fails on unexpected requestsTest error paths with flush(body, { status: 500, statusText: 'Error' }), and test interceptors the same way.
Practice. Test an API service including a 404 response and an interceptor that adds an auth header.
Debugging Tests and Code Coverage Recommended
Debugging tests: run a single file or test with fit or it.only (depending on the runner), add debugger statements, and run the tests in a browser or editor debugger. Print fixture.nativeElement.innerHTML to see what actually rendered. Most failures in component tests come from a missing provider, a missing import in the component, or waiting incorrectly for async work.
Code coverage reports, from ng test --code-coverage, show which lines and branches ran. Use them to find untested logic, not as a target to game. A common team rule is to require coverage on new code rather than a fixed global percentage.
Practice. Generate a coverage report, find the riskiest untested branch and write a test for it.
End-to-End Testing Recommended
End-to-end testing drives the real app in a browser through complete journeys such as sign-up and checkout. Protractor, the original Angular tool, is deprecated and no longer maintained.
Current choices are Playwright and Cypress, both of which can be added with ng e2e and a CLI schematic.
test('user can check out', async ({ page }) => {
await page.goto('/products/1');
await page.getByRole('button', { name: 'Add to cart' }).click();
await page.getByRole('link', { name: 'Checkout' }).click();
await expect(page.getByText('Order confirmed')).toBeVisible();
});Keep end-to-end tests few and focused on critical paths, seed predictable test data, and run them in CI against a production build.
Practice. Write Playwright tests for login and one core journey of your project and run them in CI.
Stage 9 State Management, Libraries and Builds
Goal. Choose a state approach for a large app, use and create libraries, and build and deploy for several environments.
State Management Core
State management in Angular ranges from simple to structured:
- Signals in a component for local state.
- A service with private signals and public computed values for shared state; enough for most apps.
- NgRx Signal Store, a lightweight signal-based store with methods, computed state and plugins.
- NgRx Store, a Redux-style global store with actions, reducers, selectors and effects. It adds ceremony but gives strict one-way data flow, excellent DevTools and patterns that scale across large teams.
- NGXS, a class-based store with less boilerplate than classic NgRx.
- Elf, a small, modular store built on RxJS.
export const loadProducts = createAction('[Catalogue] Load');
readonly products = this.store.selectSignal(selectProducts);Pick the simplest option that fits; a global store adopted too early mostly adds ceremony.
Practice. Build the same cart feature with a signal service and with NgRx, and compare code size and debugging.
UI Components Recommended
Most Angular apps use a component library for UI components.
- Angular Material is the official Material Design library: form fields, tables, dialogs, date pickers and theming with design tokens.
- The Component Dev Kit (CDK) provides unstyled building blocks: overlays, drag and drop, virtual scrolling, accessibility helpers such as focus traps and live announcers, and portals. It is useful even with your own design.
- PrimeNG, ng-zorro and Taiga UI are other popular libraries. Using libraries usually starts with
ng add, for exampleng add @angular/material, which installs the package and runs a setup schematic. Check that a library keeps up with each major Angular version before you depend on it, because third-party packages often block upgrades.
Practice. Build a data table with sorting, paging and a filter using Angular Material, then add CDK virtual scrolling.
Creating Libraries and Schematics Optional
Creating libraries lets several apps share components and services. In a workspace, ng generate library ui-kit creates a library project built with ng-packagr into the Angular Package Format, which works with every Angular build tool.
ng generate library ui-kit
ng build ui-kit
npm publish dist/ui-kitSchematics are code generators that create or modify files: they power ng generate, ng add and ng update migrations. You can write your own to enforce team conventions. CLI builders are the plugins behind ng build, ng serve and ng test, configured in angular.json; custom builders integrate tools the CLI does not know about.
Practice. Create a library with a button component, use it from an app in the same workspace, and add an ng add schematic.
AoT Compilation and Build Environments Core
Angular compiles templates to efficient JavaScript instructions. AoT compilation, ahead of time, does this at build time, so the browser downloads no compiler and template errors are found during the build. AoT has been the default for years; the old just-in-time mode is only for special cases.
Build environments and configuration live in angular.json. Configurations such as production and development set optimisation, source maps, budgets and file replacements.
ng build --configuration production
ng generate environments # creates environment filesEnvironment files hold build-time settings such as API URLs, swapped with fileReplacements. Never put secrets in them, because everything ends up in the browser bundle. Budgets fail the build when bundles grow past a limit, which keeps performance from slipping.
Practice. Add a staging configuration with its own API URL and a bundle budget that fails if the main bundle grows by 10 per cent.
Deployment Recommended
Deployment of a client-rendered Angular app means uploading the dist/ folder to static hosting or a CDN. Two things to configure:
- Rewrite unknown paths to
index.html, so deep links work with the router. - Cache hashed asset files for a long time and
index.htmlfor a short time, so users get new releases quickly.
Apps with server rendering need a Node.js server or a serverless platform; the CLI can generate a server entry point. ng deploy works with builders for several hosting providers.
Add the service worker with ng add @angular/pwa if you need offline support, and remember it caches aggressively.
Practice. Deploy an app to static hosting with deep-link rewrites and correct caching headers.
Stage 10 Security, Performance, SSR and Internationalisation
Goal. Ship secure, fast, accessible and multilingual Angular apps, with server rendering where it helps.
Security and Cross-site Scripting Core
Security starts with cross-site scripting (XSS), where attacker-controlled content runs as script in your page. By default Angular assumes every bound value could be hostile.
Sanitization: interpolation escapes text, and binding to [innerHTML], [src] or [href] sanitises the value, stripping script and dangerous URLs and logging a warning.
Trusting safe values: when you genuinely need raw HTML or a URL, DomSanitizer methods such as bypassSecurityTrustHtml mark a value as safe. Use them only for content you control, never for user input.
Enforce Trusted Types by setting a Content Security Policy that requires them; Angular supports Trusted Types, so the browser blocks unsafe DOM writes. Also avoid direct DOM manipulation with ElementRef.nativeElement.innerHTML, which bypasses Angular's protection, and keep Angular up to date for security fixes.
Practice. Bind user-supplied HTML with [innerHTML], inspect what Angular removes, and add a CSP with Trusted Types.
HTTP Vulnerabilities Core
HTTP vulnerabilities involve the server as much as the client.
Cross-site request forgery (CSRF) tricks a logged-in user's browser into sending an unwanted request. XSRF protection in Angular: HttpClient reads a token from a cookie, XSRF-TOKEN by default, and sends it back in a header, X-XSRF-TOKEN, on mutating requests. The server must set the cookie and check the header. The HttpClient CSRF support only applies to relative URLs, and the names can be changed with withXsrfConfiguration.
Cross-site script inclusion (XSSI) lets an attacker's page load your JSON API as a script. Servers can prefix JSON responses with )]}', and Angular's HttpClient strips that prefix automatically.
Cookies with SameSite and HttpOnly, CORS rules and authorisation checks on every endpoint complete the picture.
Practice. Configure a small back end to set an XSRF cookie and verify Angular sends the header on POST requests.
Accessibility Core
Accessibility means everyone can use the app, including keyboard and screen-reader users. Angular's templates are HTML, so the basics are the same: semantic elements, labels for inputs, enough colour contrast and visible focus.
Angular-specific points:
- Bind ARIA with attribute binding,
[attr.aria-expanded]="open()". - Use the CDK
a11ypackage for focus traps in dialogs,LiveAnnouncerfor status messages and keyboard list navigation. - Manage focus after route changes, because single-page navigation does not move it.
- Link identification: active navigation links should be identifiable to assistive technology.
routerLinkActivewithariaCurrentWhenActive="page"setsaria-currentfor you.
Test with a keyboard, a screen reader and automated checks such as axe.
Practice. Audit one page with axe and keyboard-only navigation, then fix every issue found.
Performance and Deferrable Views Core
Performance work starts with measurement: Lighthouse, the browser performance panel and the Angular DevTools profiler.
Deferrable views, @defer blocks, lazy-load part of a template and its dependencies when a trigger fires:
@defer (on viewport; prefetch on idle) {
<app-reviews [productId]="id()" />
} @placeholder { <p>Reviews</p> } @loading (minimum 300ms) { <app-spinner /> }Triggers include on idle, on viewport, on interaction, on hover, on timer and when a condition is true.
Other common fixes:
- OnPush and signals to reduce change detection work.
trackin@forso lists reuse DOM.- Slow computations: move them out of templates into
computedsignals or pure pipes, which cache results; never call expensive methods in bindings. - Lazy routes, CDK virtual scrolling for long lists and smaller dependencies.
Practice. Use @defer for a heavy chart below the fold and measure the change in initial bundle size.
Image Optimization Recommended
Image optimization in Angular centres on the NgOptimizedImage directive, used with ngSrc instead of src.
<img ngSrc="hero.jpg" width="1200" height="600" priority />
<img ngSrc="product.jpg" fill sizes="(max-width: 600px) 100vw, 50vw" />What it does:
- Requires dimensions or
fill, which prevents layout shift. - Lazy-loads images by default;
prioritymarks the largest above-the-fold image for eager loading and a preload hint. - Generates responsive
srcsetvalues when used with an image CDN loader. - Warns in development about oversized or distorted images.
Practice. Convert a product grid to NgOptimizedImage and compare Largest Contentful Paint before and after.
SSR in Angular and Hydration Recommended
SSR in Angular renders pages on the server so users and crawlers get HTML immediately. Add it with ng add @angular/ssr, or choose it in ng new.
Hydration reuses the server-rendered DOM on the client instead of destroying and recreating it, which avoids flicker and saves work. It is enabled with provideClientHydration(). Event replay records clicks that happen before the app has loaded and replays them afterwards, and incremental hydration lets @defer blocks stay dehydrated until a trigger fires.
Rules for server-safe code:
- Do not touch
window,documentorlocalStorageduring render; useafterNextRenderor check the platform. - Use
HttpClientwithwithFetch(); responses are cached into the page so the client does not repeat requests. - Keep server and client output identical to avoid hydration errors.
Practice. Add SSR to an app, fix every browser-only API that breaks it, and confirm the page renders with JavaScript disabled.
Angular SSG and AnalogJS Optional
Angular SSG, static site generation, also called pre-rendering, renders routes to HTML files at build time. It suits pages whose content changes rarely, such as marketing pages, documentation and blogs, and can be mixed with server-rendered and client-rendered routes using server route configuration that sets a render mode per route.
export const serverRoutes: ServerRoute[] = [
{ path: '', renderMode: RenderMode.Prerender },
{ path: 'account/**', renderMode: RenderMode.Client },
{ path: '**', renderMode: RenderMode.Server },
];AnalogJS is a community meta-framework for Angular, built on Vite. It adds file-based routing, Markdown content, API routes and static generation, similar to what Nuxt is for Vue. It is a good choice for content-heavy sites built with Angular, though it is not an official Angular project.
Practice. Pre-render the public pages of an app while keeping account pages client-rendered.
Internationalization Optional
Internationalization (i18n) prepares an app for several languages and regions. Angular's built-in approach uses the localize package, added with ng add @angular/localize.
- Mark text with the
i18nattribute in templates and$localizein code, with meanings and descriptions for translators. ng extract-i18nproduces translation files in XLIFF or other formats.- Locales by ID: each locale has an identifier such as
en-GBorfr-CA, which drives date, number, currency and plural formatting in pipes. - Multiple locales: the build produces a separate, fully translated bundle per locale, which is fast at run time but needs one deployment path per language.
Libraries such as Transloco load translations at run time instead, so users can switch language without a reload.
Practice. Translate a page into two locales with the built-in i18n tools and serve both builds.
Animation Optional
Animation in Angular has historically used the @angular/animations package. You define triggers with states and transitions in component metadata, and bind them in the template.
animations: [trigger('fade', [
transition(':enter', [style({ opacity: 0 }), animate('200ms', style({ opacity: 1 }))]),
transition(':leave', [animate('150ms', style({ opacity: 0 }))]),
])]
<div @fade>...</div>Transitions & triggers describe which style changes animate and when; reusable animations are defined with animation() and applied with useAnimation() so several components share them.
Recent versions add simpler native options, animate.enter and animate.leave, which apply CSS classes as elements enter and leave, and the animations package is being phased out in their favour. For new code, prefer CSS transitions and these native hooks, and respect prefers-reduced-motion.
Practice. Animate a notification list as items enter and leave, first with CSS and animate.enter, then with the animations package.
Projects to build
Build these in order. Each one should end up in a public repository with a short README that explains your decisions.
- Task managerBeginner
Build a task list with add, edit, complete, delete and filter, using standalone components, control flow blocks and a service that stores tasks in signals and persists them to local storage. Use OnPush everywhere.
Components · Control flow · Signals · Services
- Movie explorerIntermediate
Build a search app on a public API with a debounced search box, results and detail routes, lazy-loaded pages and an interceptor that adds an API key and handles errors. Cancel stale requests with
switchMapand expose data withtoSignal.Routing · HttpClient · Interceptors · RxJS operators · RxJS interop
- Multi-step booking formIntermediate
Build a three-step booking flow with typed reactive forms, cross-field and async validators, a custom date-range control using
ControlValueAccessor, and a review step. Test the form logic and each component.Reactive forms · Typed forms · Custom validators · Control Value Accessor · Testing
- Admin dashboardAdvanced
Build a dashboard with authentication guards, role-based routes, Angular Material tables with sorting and paging, deferred chart widgets and NgRx Signal Store for shared state. Profile it and keep the initial bundle under a budget.
Guards · Angular Material · Deferrable views · State management · Performance
- Server-rendered storefrontAdvanced
Build a small shop with server-rendered product pages, pre-rendered marketing pages, hydration with event replay, optimised images, two locales and Playwright tests for checkout. Add CSRF protection with a back end and deploy it.
SSR and hydration · Angular SSG · Image optimisation · Internationalization · End-to-end testing
Interview questions
Try to answer each question out loud before you open the answer.
What is dependency injection in Angular and why is it useful?Junior
Dependency injection means a class declares what it needs and Angular provides it, rather than the class creating its dependencies. Services are registered as providers, usually with providedIn: 'root', and requested with inject() or the constructor. Angular resolves them through a hierarchy of injectors, starting at the component and moving up to the root. This makes code loosely coupled and testable, since a test can provide a fake, and lets you scope instances, for example one per component subtree.
What are standalone components, and how do they differ from NgModules?Junior
A standalone component lists the components, directives and pipes it uses in its own imports array, so it does not need to be declared in an NgModule. They are the default in current Angular. NgModules grouped declarations and providers and controlled what was visible to which template, which added a layer of indirection and made lazy loading and tree shaking harder to reason about. Modules still work and appear in existing apps and some libraries, and the CLI has a migration from modules to standalone.
Explain Angular change detection and the OnPush strategy.Mid
Change detection checks component templates and updates the DOM where bound values changed. With the default strategy, every component is checked in each cycle. With OnPush, a component is checked only when an input reference changes, an event fires inside it, an async pipe emits, a signal it reads changes, or it is marked for check. OnPush is much cheaper and encourages immutable data, but mutating an input object will not update the view. I use OnPush for all components.
What are signals, and how do they compare with RxJS observables?Mid
A signal is a value container that tracks who reads it and notifies them when it changes. It always has a current value, is read synchronously by calling it, and computed derives cached values. Observables are streams of values over time, lazy, cancellable and rich in operators. I use signals for state that templates display and observables for events and async workflows such as debounced search, polling or WebSockets. toSignal and toObservable connect the two.
What is Zone.js, and what does zoneless Angular change?Senior
Zone.js patches browser async APIs so Angular knows when any timer, promise or event finished and can run change detection automatically. It is convenient but adds bundle size and triggers checks more often than necessary, for example on every mouse move a library listens to. Zoneless Angular removes Zone.js; change detection is scheduled by signals, template events, the async pipe and markForCheck. It requires state to be signal-based or OnPush-friendly, and gives faster, more predictable updates and simpler debugging.
What is the difference between switchMap, mergeMap, concatMap and exhaustMap?Mid
All four map each value to an inner observable and flatten the results; they differ when a new value arrives while an inner stream is still running. switchMap cancels the old one, which suits search and route parameters. mergeMap runs them concurrently, for independent parallel work. concatMap queues them and runs one at a time in order, for saves that must not overlap. exhaustMap ignores new values until the current one completes, which suits a submit button pressed repeatedly.
How do you avoid memory leaks from subscriptions in Angular?Mid
Prefer approaches that unsubscribe for you: the async pipe in templates, toSignal, which unsubscribes when the component is destroyed, and takeUntilDestroyed() for manual subscriptions. Operators like take(1) or first complete finite streams. HttpClient observables complete after one response, so they rarely leak, but router events, form valueChanges, intervals and WebSocket streams do not complete on their own. I avoid nested subscribe calls by using higher-order operators.
Compare template-driven and reactive forms.Junior
Template-driven forms are declared in the template with ngModel and validation attributes, and Angular builds the form model for you. They are quick for small forms but harder to test and make dynamic. Reactive forms build the model in the class with FormGroup, FormControl and FormArray; they are explicit, strongly typed, synchronous, easy to unit-test and expose valueChanges as observables. For anything beyond a simple form, I use typed reactive forms.
How do route guards work, and are they a security measure?Mid
Guards are functions the router runs before navigation. canActivate and canActivateChild allow or block a route, canMatch decides whether a route definition applies at all and can stop a lazy chunk downloading, and canDeactivate can warn about unsaved changes. A guard returns true, false or a UrlTree to redirect. They are a user-experience feature, not security: the code runs in the browser and can be bypassed, so the API must check authentication and authorisation on every request.
How does Angular protect against cross-site scripting?Mid
Angular treats every bound value as untrusted. Interpolation escapes text, and bindings to dangerous properties such as innerHTML, src and href are sanitised, removing scripts and unsafe URLs. DomSanitizer can mark a value as trusted, but that should only be used for content we control. Angular also supports Trusted Types, so a strict Content Security Policy can make the browser reject unsafe DOM writes. The main risks come from bypassing Angular, such as writing innerHTML directly or using bypassSecurityTrust on user input.
What are deferrable views and when would you use them?Mid
A @defer block lazy-loads a part of a template, with its components, directives and pipes, when a trigger fires: when the browser is idle, the block enters the viewport, the user interacts or hovers, after a timer, or when a condition becomes true. It has optional placeholder, loading and error blocks, and can prefetch code earlier than it renders. I use it for heavy content below the fold, such as charts, comments and maps, to shrink the initial bundle. With SSR, it also enables incremental hydration.
How do server-side rendering and hydration work in Angular?Senior
With @angular/ssr, the server runs the app for a request and sends HTML, so users and crawlers see content before JavaScript loads. On the client, hydration with provideClientHydration() reuses that DOM instead of rebuilding it, and attaches listeners. Event replay captures early clicks, and HTTP responses fetched on the server are transferred so the client does not repeat them. Code must not use browser globals during render, and server and client output must match, or hydration errors appear.
How would you structure state in a large Angular application?Senior
I separate kinds of state. Local UI state stays in component signals. Server data is fetched through services, cached with resources or a query library, and not copied into a global store without reason. Shared client state, such as the current user or a cart, lives in small services with private signals and public computed values, or in NgRx Signal Store per feature. I would use classic NgRx Store when a large team needs strict patterns, action history and DevTools, accepting the extra code. Features stay lazy-loaded with their own state.
Common mistakes
- Learning from old tutorials that use NgModules everywhere,
*ngIfand Zone-dependent tricks; start with standalone components, control flow blocks and signals. - Subscribing manually and never unsubscribing; use the
asyncpipe,toSignalortakeUntilDestroyed. - Nesting
subscribecalls inside each other; chain the streams withswitchMap,concatMapor another higher-order operator. - Calling expensive methods in template bindings, which run on every check; use
computedsignals or pure pipes. - Mutating arrays and objects in OnPush components and wondering why the view does not update; create new references or use signals.
- Putting HTTP calls and business logic inside components; move them into services so they can be reused and tested.
- Treating route guards as security; always enforce authorisation on the server.
- Using
bypassSecurityTrustHtmlor directinnerHTMLwrites on user content; let Angular sanitise, or sanitise on the server.
Ready-for-interview checklist
- I can create a project with the CLI and explain the purpose of each generated file.
- I can build standalone components with signal inputs, outputs,
model()and content projection. - I can use all template binding types and the
@if,@for,@switch,@deferand@letblocks. - I can explain dependency injection, provider types, scopes and the injector hierarchy.
- I can use
signal,computed,effectandlinkedSignal, and explain when to use each. - I can explain change detection, OnPush, Zone.js and zoneless apps.
- I can choose the right RxJS operator for search, saving, polling and combining streams, and avoid leaks.
- I can set up routing with lazy loading, guards and route input binding.
- I can build typed reactive forms with custom and async validators and a custom form control.
- I can call APIs with HttpClient, write interceptors and test HTTP code.
- I can test services, components, directives and pipes, and write an end-to-end test.
- I can explain Angular's protection against XSS and CSRF and where it stops.
- I can improve performance with deferrable views, image optimisation and lazy loading, and add SSR with hydration.
Related roadmaps
- TypeScript RoadmapTypes, generics, narrowing, utility types and project setup.
- JavaScript RoadmapLanguage core, async, the event loop, modules and the DOM.
- React RoadmapComponents, hooks, state, data fetching, performance and testing.
- Vue RoadmapReactivity, components, Composition API, Pinia and Nuxt.
- Node.js RoadmapEvent loop, modules, HTTP, streams, frameworks and deployment.
- CSS RoadmapCascade, layout with flexbox and grid, responsive design, animation.
FAQ
How long does it take to learn Angular?
With good JavaScript and basic TypeScript, you can build simple Angular apps in three to four weeks. Reaching the level expected for a junior or mid-level role, including RxJS, routing, forms, testing and change detection, usually takes three to four months at around eight hours a week. RxJS is often the slowest part, so practise it early and often.
Should I learn TypeScript before Angular?
Learn the basics first: types, interfaces, classes and generics. Angular uses TypeScript everywhere, including decorators and dependency injection, so knowing the language makes the framework far easier. The TypeScript Roadmap covers what you need; you can learn the advanced type features later.
Is Angular still worth learning?
Yes. Angular is actively developed, with a predictable release cycle and major modernisation in recent versions, and it is widely used in large companies, finance, government and enterprise software, where codebases live for many years. Check job listings in your area to see how it compares with React and Vue there.
Do I still need to learn RxJS now that Angular has signals?
Yes, at least the core. Signals handle most component state, but HttpClient, the router and reactive forms still use observables, and many existing codebases rely heavily on RxJS. Learn observables, subscription clean-up and about ten common operators, plus the interop functions that convert between observables and signals.