AngularAtlased. 22.2

Ch. 12SSR & hydration54 / 75

SSR, prerender & render modes

When to render on the server, when at build time and when only in the browser: RenderMode per route, app.routes.server.ts and the real costs.

v22architecture ~0 min read Official docs

By default, Angular delivers the application as CSR (client-side rendering): the server sends an empty <app-root></app-root>, and the browser has to download, parse and run all the JavaScript — and then fetch the data too — before it shows anything useful. SSR (rendering on the server, on each request) and SSG (prerender, at build time) move the first render elsewhere: the user receives HTML with content straight away.

With @angular/ssr you don’t choose once for the whole application, but per route: the file app.routes.server.ts says for each path whether it is RenderMode.Prerender, RenderMode.Server or RenderMode.Client. This is called hybrid rendering. The server routes API appeared in v19 and has been stable since v20.

In short
  • Rendering in the browser (CSR), on the server (SSR) or at build time (prerender) is chosen per route, with RenderMode in app.routes.server.ts.
  • Prerender pages that look the same for everyone, SSR pages that are personalised or have fast-changing data, CSR for the areas behind login.
  • SSR improves the first paint and what a crawler sees, but it adds a server to maintain and code that must also run on the server.
  • Routes with parameters can be prerendered if you give them the list of values, with getPrerenderParams.
  • CDN caching often matters more than server speed.
Why it matters

SSR is not a free performance switch. You gain LCP, SEO and correct previews on social networks; you pay with a Node server to operate, with code that must run without window, and with a window in which the page is visible but still doesn’t respond to clicks. The right decision is made per route, not per project.

Three rendering modes

CSR · ClientSSR · ServerSSG · Prerender
The HTML appearsin the browser, after JSon the server, on each requestat build time, once
The server, per requestserves a static file (the empty shell)starts the application, asks for the data, rendersserves a static file
TTFBsmallincludes the rendering and the APIs it callsminimal, especially from a CDN
LCPafter JS + bootstrap + API requestas soon as the HTML arrivesas soon as the HTML arrives
SEO, previewsweak: depends on JSexcellentexcellent
Content per useryesyesno — identical HTML for everyone
Hostingany static hosting / CDNa Node runtime (or another JS runtime)any static hosting / CDN
Cost per request≈ 0CPU + memory for a full render≈ 0

What the server concretely does in Server mode: for each request, @angular/ssr creates a new platform, calls the bootstrap function from main.server.ts, lets the router navigate to the URL, waits for the application to become stable (all HTTP requests and PendingTasks finished), serialises the DOM into HTML and destroys the application. The practical consequence: the TTFB of an SSR page is at least as long as the slowest API called during rendering.

In a real hybrid application the modes coexist: the homepage and the marketing pages are prerendered, search is rendered on the server, and the account area runs only in the browser.

What happens on a request

Five moments matter, in the order the user experiences them:

  • TTFB (time to first byte) — the first byte of the HTML response.
  • FCP (first contentful paint) — the first text or the first image on screen.
  • LCP (largest contentful paint) — the main element appears (the title, the product image). It’s the metric the user feels as “it has loaded”.
  • Hydration — the JS has arrived, Angular takes over the existing HTML and attaches the listeners.
  • Interactive — a click actually does something. From this moment INP matters (how quickly the page responds).

With SSR and SSG, between LCP and “interactive” there is a window in which the page looks ready, but the buttons don’t have a listener yet. On a modest phone, with a bundle of a few hundred KB, the window easily lasts 1–3 seconds. Run the simulation and compare the three modes on the same network.

LiveThe timeline of a request (simulation)
magazin.ro/produse/42
Shop
Wireless headphones Aria X2
349 RON · delivery tomorrow
interactive
0 ms500 ms1.00 s1.50 s2.00 s
Server: API + render
HTML · 40 KB
main.js · 260 KB
Hydration
TTFB 260 msFCP = LCP 535 msinteractive 2.36 snot interactive 1.82 s

SSR: TTFB includes the server’s work (which waits for the API), then the content appears immediately. Yellow band = content visible but without listeners until hydration.

Choose the mode and the network, then drag the cursor. The yellow band is the “visible, but not interactive” window; press “Add to cart” during it. The numbers are illustrative, not measurements.

Notice what does not change: main.js is the same size in all three modes. SSR doesn’t shrink the JavaScript, so interactivity comes at about the same time as with CSR — sometimes even a little later, because the HTML is bigger and hydration costs CPU too. The real gain is the LCP. To shrink the non-interactive window: less initial JS (lazy routes, @defer), incremental hydration and event replay, which doesn’t lose the clicks made in the window.

What a crawler sees

A crawler starts from the HTML it receives (what you see with View source, not in the Elements tab). With CSR, that HTML is identical for any URL. Googlebot can run JavaScript, but at a later stage and with limits; bots that generate previews in chats and social networks usually read only the initial HTML. There is one detail that is often ignored: the status code. A static server doesn’t know that /produse/9999 doesn’t exist, so it answers 200 — a “soft 404”.

LiveWhat a crawler receives: CSR vs SSR vs SSG (simulation)
curl https://magazin.ro/produse/42
HTTP/1.1 200 OK<!doctype html><html lang="en"><head>  <title>Shop</title>  <link rel="stylesheet" href="styles-5INURTSO.css"></head><body>  <app-root></app-root>  <script src="main-HG7KQWZB.js" type="module"></script></body></html>
Preview in a chat (a bot that only reads the HTML)
no image
magazin.ro
Shop
— no description —
  • ✓Correct HTTP status200
  • ✕page-specific <title>Shop
  • ✕meta description / og:*missing
  • ✕Visible text in the HTML0 words
  • ✕Links <a href> to follow0

The same HTML for every URL: all the content appears only after JavaScript runs. Preview bots don’t run it; Googlebot does, but at a later stage and with limits.

Switch the mode and tick “nonexistent product”. Watch the status, the title, the meta tags and the markers added by Angular (ngh, jsaction, ng-state).

The title and meta tags are set with the Title and Meta services from @angular/platform-browser. They work through DOCUMENT, so they also work on the server: whatever you set during rendering ends up in the HTML that is sent. For pages without data, the route’s title property is enough (or your own TitleStrategy, if you want a common format).

import { Component, computed, effect, inject, input } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { Meta, Title } from '@angular/platform-browser';

interface Product { id: string; name: string; description: string; imageUrl: string }

@Component({
  selector: 'app-product-page',
  template: `
    @if (product.hasValue()) {
      <h1>{{ product.value().name }}</h1>
      <p>{{ product.value().description }}</p>
    }
  `,
})
export default class ProductPage {
  readonly id = input.required<string>(); // from the route produse/:id (withComponentInputBinding)

  private readonly title = inject(Title);
  private readonly meta = inject(Meta);
  protected readonly product = httpResource<Product>(() => `/api/products/${this.id()}`);

  constructor() {
    // Runs on the server too: the tags end up in the HTML sent to the crawlers.
    effect(() => {
      if (!this.product.hasValue()) return;
      const p = this.product.value();
      this.title.setTitle(`${p.name} — Shop`);
      this.meta.updateTag({ name: 'description', content: p.description });
      this.meta.updateTag({ property: 'og:title', content: p.name });
      this.meta.updateTag({ property: 'og:description', content: p.description });
      this.meta.updateTag({ property: 'og:image', content: p.imageUrl });
    });
  }
}
One place for SEO

Put the logic in a service SeoService.update({ title, description, image, canonical }) and call it from the detail pages. For <link rel="canonical"> you need DOCUMENT — details on the Server-compatible code page.

Setup: what ng add generates

# new project, with SSR + prerender configured from the start
ng new magazin --ssr

# existing project: add @angular/ssr, express and the server files
ng add @angular/ssr

# production build → dist/magazin/browser + dist/magazin/server
ng build

# start the Node server (script added by ng add in package.json)
npm run serve:ssr:magazin        # = node dist/magazin/server/server.mjs

The command adds the dependencies (@angular/ssr, express), four files and a few options in angular.json. It’s worth knowing what each one does, because you will modify them:

  • server.ts — the Express server. It serves the static files from dist/…/browser, and gives the rest of the requests to AngularNodeAppEngine.handle(), which returns a Response (or null if the URL isn’t an Angular route). The reqHandler export is used by the CLI: ng serve renders on the server too.
  • main.server.ts — the bootstrap function the server calls on every render.
  • app.config.server.ts — provideServerRendering(withRoutes(serverRoutes)), combined with the browser config through mergeApplicationConfig.
  • app.routes.server.ts — the render mode per route. By default, everything is Prerender.
  • app.config.ts gets provideClientHydration(), so the browser reuses the HTML instead of rebuilding it.
import {
  AngularNodeAppEngine,
  createNodeRequestHandler,
  isMainModule,
  writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';
import { join } from 'node:path';

const browserDistFolder = join(import.meta.dirname, '../browser');

const app = express();
const angularApp = new AngularNodeAppEngine(); // one instance for the whole server

// 1. (optional) your APIs: app.get('/api/{*splat}', (req, res) => { ... });

// 2. Static files (JS/CSS/images have a hash in the name → one-year cache)
app.use(express.static(browserDistFolder, { maxAge: '1y', index: false, redirect: false }));

// 3. Anything else: Angular decides — prerendered HTML, SSR or the CSR shell
app.use((req, res, next) => {
  angularApp
    .handle(req)
    .then((response) => (response ? writeResponseToNodeResponse(response, res) : next()))
    .catch(next);
});

if (isMainModule(import.meta.url) || process.env['pm_id']) {
  const port = process.env['PORT'] || 4000;
  app.listen(port, (error) => {
    if (error) throw error;
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

// used by ng serve / ng build (and by some serverless platforms)
export const reqHandler = createNodeRequestHandler(app);

A detail in server.ts: express.static has index: false, so not even / is served directly from disk. Every page goes through Angular, which applies the render mode, the headers and the status configured for the route.

allowedHosts in production (v22)

To prevent SSRF, the server validates the Host header. From v22, an unknown host gets 400 Bad Request (before, the server fell back to CSR). ng add puts "allowedHosts": []: fill in your domains in angular.json (wildcards such as *.magazin.ro are accepted), in the options of AngularNodeAppEngine or in the environment variable NG_ALLOWED_HOSTS. Behind a load balancer, the X-Forwarded-* headers are ignored until you explicitly allow them with trustProxyHeaders. Details in Security.

If you have an older project whose server.ts uses CommonEngine: that API is deprecated since v22, its replacement being AngularNodeAppEngine (Node) or AngularAppEngine (runtimes based on the Web API Request/Response).

RenderMode per route

import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  // The same for everyone, changes only at deploy → HTML generated at build time (SSG)
  { path: '', renderMode: RenderMode.Prerender },
  { path: 'despre', renderMode: RenderMode.Prerender },

  // Public, indexed, data that changes often → rendered on every request (SSR)
  { path: 'cautare', renderMode: RenderMode.Server },

  // Behind the login: no SEO, personal HTML → browser only (CSR)
  { path: 'cont/**', renderMode: RenderMode.Client },

  // The safety net for any Angular route not listed above
  { path: '**', renderMode: RenderMode.Server },
];

The rules that matter in practice:

  • Matching: an exact segment beats a parameter (:id), which beats **. So ** is the safety net, not a rule that “overrides”.
  • Two-way validation: every route in the router must match a server route, and every server route (except those ending in /**) must exist in the router. Otherwise the build stops with an explicit message.
  • The path doesn’t start with “/” — '/despre' is a build error.
  • What the browser receives: Prerender → the file generated at build time (with an ETag); Server → HTML rendered now; Client → index.csr.html, the empty shell, also served by your server.
LiveWhich render mode for this route? (simulation)
Examples:
RecommendationRenderMode.PrerenderSSG at build + SSR for the rest
  • The HTML is identical for everyone and stable: you generate it once, at build time.
  • Served as a static file (ideally from a CDN): minimal TTFB, almost zero cost per request.
Watch out for
  • The data changes daily → you need a scheduled build (e.g. nightly in CI) that regenerates the pages.
  • Thousands of pages add minutes to the build; measure the build time and the size of the deploy.
import { PrerenderFallback, RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    path: 'produse/:id',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const api = inject(CatalogApi); // synchronous, before the await
      const items = await api.mostVisited(500);
      return items.map((x) => ({ id: String(x.id) }));
    },
    fallback: PrerenderFallback.Server,
  },
  // ...
];
Try the examples or describe your own route. The recommendation and the entry in app.routes.server.ts update live; “Watch out for” shows the hidden cost of the choice.

Headers, status and redirects

Each ServerRoute can have headers and status. The exception: Prerender routes accept headers, but not status — the ServerRoutePrerender type excludes it, because the file is generated only once. The headers of a prerendered page apply only when it is served through the Node server, not from static hosting.

import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    // Public HTML: the CDN can keep it for 60 s and serve the old version while it refreshes
    path: 'cautare',
    renderMode: RenderMode.Server,
    headers: { 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300' },
  },
  {
    // permanent redirect: { path: 'oferte', redirectTo: 'promotii' } in app.routes.ts
    path: 'oferte',
    renderMode: RenderMode.Server,
    status: 301, // without status: 302
  },
  {
    // prerender: headers yes, status NO (the ServerRoutePrerender type excludes it)
    path: 'despre',
    renderMode: RenderMode.Prerender,
    headers: { 'X-Frame-Options': 'DENY' },
  },
  { path: '**', renderMode: RenderMode.Server },
];

A redirectTo from the router becomes a real HTTP redirect on the server (302 by default; you can set status: 301, 303, 307 or 308), answered without rendering the application. Likewise, if a guard redirects during rendering (see Guards & resolvers), the final URL differs from the requested one and the server answers with a redirect. With outputMode: 'static', where there is no server, redirects are prerendered as “soft redirects” with a <meta http-equiv="refresh"> in the HTML.

Status and headers decided at runtime

When the status depends on data (the product doesn’t exist), you set it from the component through RESPONSE_INIT. For cookies, language or user agent you read the request through REQUEST. Both come from @angular/core and are null in the browser, at prerender and at build time — always handle the null case.

import { Component, RESPONSE_INIT, inject } from '@angular/core';

@Component({
  selector: 'app-not-found',
  template: `<h1>Page not found</h1>`,
})
export default class NotFound {
  constructor() {
    // RESPONSE_INIT is null in the browser, at prerender and at build → always check.
    const response = inject(RESPONSE_INIT);
    if (response) response.status = 404; // the crawler sees 404, not “200 with an error text”
  }
}

Prerender with parameters

Routes without parameters are discovered and prerendered automatically. For produse/:id, Angular can’t guess which IDs exist, so you give it a function getPrerenderParams. It runs at build time, in an injection context (you can use inject()), and returns a list of parameter objects → value. A Prerender route with parameters but without getPrerenderParams stops the build.

import { inject } from '@angular/core';
import { PrerenderFallback, RenderMode, ServerRoute } from '@angular/ssr';
import { ProductApi } from './products/product-api';

export const serverRoutes: ServerRoute[] = [
  {
    path: 'produse/:id',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const api = inject(ProductApi); // ✓ synchronous inject(), BEFORE any await
      const ids = await api.topProductIds(500); // runs at BUILD time, not on request
      return ids.map((id) => ({ id: String(id) })); // the values must be strings
    },
    // /produse/9999 was not prerendered → what do we do on request?
    fallback: PrerenderFallback.Server, // default: SSR on request
  },
  {
    path: 'blog/:slug/**', // parameters + catch-all
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      return [{ slug: 'ssr', '**': 'partea/1' }]; // → /blog/ssr/partea/1
    },
  },
  { path: '**', renderMode: RenderMode.Server },
];

What happens to a URL that wasn’t generated at build time depends on fallback:

PrerenderFallbackFor /produse/9999 (not prerendered)When to choose it
Server (default)rendered on request, on the servera large catalogue: prerender the top N, the rest on request
Clientreceives the CSR shell, rendered in the browseryou want SEO only for the generated pages and don’t want SSR
NoneAngular doesn’t answer (the request goes on, usually a 404)the list from the build is complete by definition
inject() after await

inject() works only synchronously, at the beginning of the function. An inject() written after an await is no longer in an injection context and throws an error. Take all the dependencies in the first lines, then make the requests.

The cost is at build time: every URL is a full render. Ten thousand products mean ten thousand renders on every deploy and ten thousand files to upload. If the data changes daily, you also need a scheduled build that regenerates them.

Deploy, cache and costs

outputMode in angular.json decides what the build produces. With "server" (what ng add sets) you get dist/app/browser plus dist/app/server/server.mjs, and you need a Node process. With "static" you get only files: prerendered pages and the CSR shell, deployable on any CDN. A RenderMode.Server route in static mode is a build error.

{
  "projects": {
    "magazin": {
      "architect": {
        "build": {
          "options": {
            "outputMode": "static"
          }
        }
      }
    }
  }
}

The cache layers

WhatHowWatch out
JS, CSS, images with a hashmaxAge: '1y' in express.static / CDNthe names change on every build, so a long cache is safe
Prerendered HTMLstatic file, ETag → 304 Not Modifiedchanges only at deploy
Public SSR HTMLheaders per route: public, s-maxage=60, stale-while-revalidate=300only for content identical for everyone
Personal SSR HTMLprivate, no-storenever public: the CDN would serve it to another user
Data HTTP during renderingtransfer cache: GET/HEAD responses end up in the HTML and are reused at hydrationrequests with Authorization/Cookie are excluded by default

The transfer cache is the reason why, in the timeline, SSR no longer makes the API request from the browser: the data fetched by HttpClient on the server is serialised into <script id="ng-state"> and read at startup. Its options (withHttpTransferCacheOptions, withNoHttpTransferCache) are on the Hydration page. Also: on the server, with the default fetch backend, each HTTP response is limited to 1 MB (error NG02825); the limit changes with provideServerRendering({ maxResponseBodySize }, …).

Operational cost

  • CPU per request: each SSR render is a full bootstrap. Without a CDN cache, bot traffic is paid for in servers.
  • TTFB tied to the backend: the server waits for stability, so a slow API slows down all your pages. Put timeouts in place and move secondary data into @defer (on the server, a block without a hydrate trigger renders only the placeholder).
  • Memory shared between requests: the code is evaluated once per process, so module-level variables and useValue providers are common to all users.
  • One more system in production: logs, monitoring, scaling, Node version, cold start on serverless.
import { InjectionToken } from '@angular/core';

// ✗ Module-level state: on the server the module is evaluated ONCE,
//    so the variable is shared by all requests (and all users).
let currentUser: string | null = null;

// ✗ useValue at application level: the same value until the server restarts.
export const RENDERED_AT = new InjectionToken<number>('RENDERED_AT');
const bad = { provide: RENDERED_AT, useValue: Date.now() };

// ✓ useFactory: runs on every render (each request has its own injector).
const good = { provide: RENDERED_AT, useFactory: () => Date.now() };

// ✓ Per-user state lives in services (instantiated per request), not in global variables.

When YES, when NO

SSR / SSG is worth it
  • Public pages that must be indexed and shared: products, articles, landing pages. Prerender if you can generate them at build time, Server if not.
  • LCP translates into money (e-commerce, media), and some users have modest phones and networks.
  • Stable content (documentation, blog, marketing): Prerender + outputMode: 'static', with no server to maintain.
  • Public content with fresh data and heavy traffic: Server + a short cache at the CDN.
  • You need correct status codes (404, 301) for SEO.
Stay with CSR (or choose something else)
  • Applications behind the login (dashboard, admin, back office): Client. Invest in a small bundle, lazy routes and @defer.
  • You have nobody to operate a Node server: stay with CSR or with Prerender and outputMode: 'static'.
  • “The application is slow after loading”: SSR doesn’t help after hydration. The problem is in the JS or in change detection → Performance, Change detection.
  • Routes built on browser APIs (maps, canvas, editors): RenderMode.Client on that route.
  • Personalised HTML with heavy traffic: don’t render per user. Prerender the common part and load the personal pieces in the browser.

Common pitfalls

window is not defined

Code that touches window, document or localStorage in the constructor, in ngOnInit or at module level crashes on the server. The solution: afterNextRender, the DOCUMENT token and separate service implementations for the server and the browser — all on the Server-compatible code page.

Data mixed up between users

A let currentUser variable at module level, or a useValue computed at startup, lives as long as the Node process, not as long as the request. Put the state in services (instantiated per render) and use useFactory for values computed per request.

  • Public cache on personalised HTML → one user sees another user’s page. On Server routes with personal data use Cache-Control: private, no-store.
  • Empty allowedHosts in production → 400 on all pages (v22). Configure the domains or NG_ALLOWED_HOSTS.
  • “Soft 404”: the “not found” component answers 200. Set status = 404 through RESPONSE_INIT.
  • Wrong SPA fallback on static hosting: with prerender, index.html is the page of the / route. Client routes must fall back to index.csr.html.
  • The application doesn’t become stable: a PendingTask without cleanup (or, with Zone.js, a setInterval started in the Angular zone) keeps the rendering waiting, and the request never finishes.
  • A CDN that minifies the HTML and strips comments → hydration fails with NG0507. Angular’s comments are needed; disable the option on the CDN.
  • “I added SSR and TTI stayed the same” — that’s normal: the JS is the same. Measure the LCP, not the TTI, and reduce the initial JS separately.

Cheat sheet

// ng new app --ssr  |  ng add @angular/ssr  |  ng build  |  node dist/app/server/server.mjs

// app.config.server.ts
provideServerRendering(withRoutes(serverRoutes));
provideServerRendering({ maxResponseBodySize: 5 * 1024 * 1024 }, withRoutes(serverRoutes)); // default 1 MB

// app.config.ts (browser)
provideClientHydration();

// app.routes.server.ts — import { RenderMode, PrerenderFallback, ServerRoute } from '@angular/ssr'
export const serverRoutes: ServerRoute[] = [
  { path: 'despre', renderMode: RenderMode.Prerender }, // HTML at build (SSG)
  {
    path: 'produse/:id',
    renderMode: RenderMode.Prerender,
    getPrerenderParams: async () => [{ id: '1' }, { id: '2' }], // runs at build
    fallback: PrerenderFallback.Server, // | Client | None — default Server
  },
  { path: 'cautare', renderMode: RenderMode.Server, headers: { 'Cache-Control': 'no-store' } },
  { path: 'cont/**', renderMode: RenderMode.Client }, // empty shell, rendered in the browser
  { path: '**', renderMode: RenderMode.Server }, // the safety net
];

// @angular/core — all null in the browser, at prerender and at build
inject(REQUEST); // Request: headers, cookies, URL
inject(RESPONSE_INIT); // ResponseInit: status / headers set dynamically
inject(REQUEST_CONTEXT); // the second argument of angularApp.handle(req, context)

// angular.json → architect.build.options
//   "outputMode": "server" | "static"               static = no Node: only Prerender + Client
//   "security": { "allowedHosts": ["exemplu.ro"] }  unknown host → 400 Bad Request (v22)

Ch. 12 · 0 of 3 complete

Finished this lesson?

Mark it and it shows up on your map: in the contents and on the home page.

Marks itself as done once you have read the whole lesson.

  1. SSR, prerender & render modes
  2. Hydration: event replay & incremental
  3. Server-compatible code
See your whole map

All 75 pages · type to filter

  • 01What's new in Angular 21 & 22All the important changes in v21 and v22, briefly, with a link to the page that explains each one.Start
  • 01Your first app, step by stepA reading list built from an empty component to a finished app — signals, @for, inputs and outputs, a service and a form, one step at a time.Start
  • 01Quick decisions“Where do I put this?”, “Signal or Observable?”, “Shared or feature?” — short answers to everyday questions.Start
  • 01Angular interview questions60 real interview questions for junior, mid and senior, with the answer to give, the follow-up and the red flags. Plus a mock interview.Start
  • 02Standalone & bootstrapHow an app starts without NgModule: bootstrapApplication, app.config.ts, imports in the component.Modern fundamentals
  • 02Data binding & eventsInterpolation, property and attribute bindings, events with $event and key filters, and how [( )] is built — what goes into the DOM and what comes back.Modern fundamentals
  • 02Control flow: @if, @for, @switchThe block syntax that replaces *ngIf/*ngFor/ngSwitch, with track, @empty and exhaustive checks.Modern fundamentals
  • 02@let and the new template expressionsLocal variables with @let, arrow functions, spread, regex, instanceof, comments inside tags.Modern fundamentals
  • 02@defer — lazy loading in templatesLoad heavy components only when needed: triggers, prefetch, placeholder, loading, error.Modern fundamentals
  • 02Class & style bindingsWhy [class.x] and [style.x] replaced ngClass and ngStyle (with automatic migration from v21).Modern fundamentals
  • 02Component styles & encapsulation:host, the three encapsulation modes, theming with CSS variables, and why ::ng-deep is a last resort.Modern fundamentals
  • 02Pipes: when and howPure vs impure pipes, custom pipes, and how you choose between a pipe, computed() and a method.Modern fundamentals
  • 03signal() and computed()The reactive primitives: writable state, derived state, equality, untracked and immutability rules.Signals & reactivity
  • 03effect() — when YES, when NOSide effects synchronised with signals: localStorage, logging, imperative APIs. And their traps.Signals & reactivity
  • 03linkedSignal() — editable derived stateA signal that resets when its source changes, but that you can also set manually.Signals & reactivity
  • 03resource, rxResource, httpResourceAsync data as signals: status, value, error, reload, automatic cancellation. Stable since v22.Signals & reactivity
  • 03debounced() — delayed signalsThe “debounce” version of a signal, without RxJS. Perfect for search.Signals & reactivity
  • 03Lists: search, filter, sort, paginateA chain of computed signals that recalculates only what changed, with the filters kept in the URL so a link reopens the same view.Signals & reactivity
  • 03Observables from zeroWhat an Observable is, subscribe and unsubscribe, the operators you use every week, cold vs hot — and when a signal is enough.Signals & reactivity
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — and when RxJS remains the right choice.Signals & reactivity
  • 04input() and output()Inputs as signals (required, transform, alias) and typed outputs. Data down, events up.Components: communication
  • 04model() — two-way bindingBanana-in-a-box [( )] with a single API. When to use it, and when input + output is better.Components: communication
  • 04viewChild & contentChildQueries as signals: viewChild, viewChildren, contentChild, contentChildren, the read option.Components: communication
  • 04Lifecycle & DestroyRefWhich hooks still matter in the signals era, afterNextRender, DestroyRef and typed ngOnChanges.Components: communication
  • 04Content projectionng-content with select, multiple slots, default content and the trap of conditional projection.Components: communication
  • 04ng-template & ngTemplateOutletTemplates as values: render props in Angular, typed context and customising components.Components: communication
  • 05Smart vs DumbContainer vs presentational: who holds the state, who only displays, and how to split them properly.Component design
  • 05Generic components <T>A correctly typed DataTable<T> or Select<T>, with templates for cells.Component design
  • 05Dynamic componentsNgComponentOutlet, createComponent with inputBinding/outputBinding, and a dashboard built from config.Component design
  • 05Directives & host compositionAttribute directives, the host: {} metadata and the Directive Composition API (hostDirectives).Component design
  • 05Custom form controlsFormValueControl (Signal Forms) vs ControlValueAccessor: a star rating, in both variants.Component design
  • 05Change detectionOnPush is the default from v22, zone.js is gone from v21. What triggers rendering and how not to get burned.Component design
  • 06inject() & @Service()The new @Service() decorator from v22, inject() vs the constructor, and the injection context.Dependency Injection
  • 06Providers & the injector hierarchyRoot vs route vs component, useClass/useValue/useFactory/useExisting, and the optional/self/skipSelf modifiers.Dependency Injection
  • 06InjectionToken & provideX()Typed configuration, provideFeature() functions, makeEnvironmentProviders and initializers.Dependency Injection
  • 06injectAsync() — lazy servicesLoad a heavy service only when you need it, with prefetch on idle.Dependency Injection
  • 07Folder structurecore / shared / features, what goes into each, and what a feature looks like inside.Application architecture
  • 07Shared vs featureWhen to move a component into shared and (especially) when NOT to. A concrete checklist.Application architecture
  • 07Interfaces & modelsInline interface or separate file? DTO vs domain model, type vs interface, where to put them.Application architecture
  • 07State management with signalsSimple signal stores, facades, local vs global state, and when you need NgRx.Application architecture
  • 07Style guide & namingThe current official conventions: file names without suffixes, protected, readonly, member order.Application architecture
  • 07Barrels, aliases & boundariesindex.ts, path aliases, a feature’s public API and dependency rules that can be checked automatically.Application architecture
  • 07Errors: @boundary & ErrorHandlerThe new @boundary block from v22.2, the global ErrorHandler, HTTP errors and errors from resource.Application architecture
  • 08Modern routing & lazy loadingloadComponent, child routes, parameters as inputs and parameter inheritance (the new default in v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate and resolve as functions, with redirects via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — the basicsForms built on signals: form(), [formField], schema-based validation. Stable since v22.Forms
  • 09Signal Forms — advancedCustom and cross-field validation, async/HTTP with debounce, dynamic lists, disabled/hidden, reusable schemas.Forms
  • 09Typed Reactive FormsTyped FormGroup, nonNullable, FormArray — and how they coexist with Signal Forms.Forms
  • 10HttpClient, interceptors & the API layerHttpClient without setup (v21), Fetch by default (v22), functional interceptors and an API layer with DTO mapping.HTTP & data
  • 11Performance — a checklistWhat really matters: OnPush + signals, track, @defer, lazy routes, images, bundles and hydration.Quality & performance
  • 11Testing with VitestVitest is the default runner from v21. TestBed, components with signals, services, HTTP and harnesses.Quality & performance
  • 11DevTools: look inside your appChrome DevTools and Angular DevTools step by step: what each panel shows, with exercises that work on this site.Quality & performance
  • 11Angular AriaHeadless, accessible components that you style however you like: tabs, accordion, menu, listbox...Quality & performance
  • 12SSR, prerender & render modesWhen to render on the server, when at build time and when only in the browser: RenderMode per route, app.routes.server.ts and the real costs.SSR & hydration
  • 12Hydration: event replay & incrementalHow the browser takes over server HTML without re-rendering it: full hydration, event replay, @defer (hydrate on …) and mismatch errors.SSR & hydration
  • 12Server-compatible codewindow doesn’t exist on the server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, the HTTP cache and PendingTasks.SSR & hydration
  • 13Micro frontendsWhen it is worth splitting an app into several deployments and when it is just complexity: Native Federation, Module Federation, iframes, Web Components.Architecture at scale
  • 13Web Components with Angular ElementscreateCustomElement: an Angular component used as an HTML tag anywhere, plus communication between apps through events and BroadcastChannel.Architecture at scale
  • 13Monorepo, libraries & boundariesA workspace with several apps and libraries: ng generate library, Nx, secondary entry points, publishing and dependency rules.Architecture at scale
  • 13Hexagonal architecture (ports & adapters)A domain without Angular, ports as abstract classes and interchangeable adapters: HTTP, localStorage or in-memory, without touching the UI.Architecture at scale
  • 14Signals under the hoodThe reactive graph: producers and consumers, push-pull, why computed is lazy and glitch-free, the diamond problem and when effects run.Advanced reactivity
  • 14Advanced RxJS: higher-order operatorsswitchMap, mergeMap, concatMap and exhaustMap on a live diagram, plus retry with backoff, shareReplay and how to avoid leaks.Advanced reactivity
  • 14NgRx SignalStoreLarge-scale state management on signals: withState, withComputed, withMethods, rxMethod, entities and hooks — and when a simple store is enough.Advanced reactivity
  • 14State machines with signalsNo more impossible states: a typed machine on signals, with transitions, guards and a live state diagram.Advanced reactivity
  • 15WebSockets: real time that survives the networkA typed socket as signals: reconnect with backoff and jitter, heartbeat, an outbox while offline, and SSR safety.Real time & offline
  • 15Optimistic updates & rollbackAn instant UI that stays honest: snapshot and rollback, out-of-order responses, idempotency keys.Real time & offline
  • 15Offline-first: Service Worker & cachingThe Angular Service Worker: caching strategies, update prompts and an outbox for changes made offline.Real time & offline
  • 16Web WorkersHeavy computation without blocking the UI: a real worker in the page, message passing and when it is not worth it.Advanced UI & performance
  • 16Huge lists: virtual scroll100,000 rows without lag: CDK virtual scroll, itemSize, trackBy and the alternatives (pagination, infinite scroll).Advanced UI & performance
  • 16Modern animations & View Transitionsanimate.enter and animate.leave instead of the deprecated @angular/animations, CSS transitions and View Transitions on navigation.Advanced UI & performance
  • 16Memory leaks: finding and fixing themSubscriptions, timers and listeners that outlive their component — how to spot them in DevTools and close them.Advanced UI & performance
  • 17Security: XSS, sanitisation & CSPWhat Angular does automatically against XSS, when bypassSecurityTrust becomes dangerous, plus CSP with nonce, Trusted Types and XSRF.Security & globalisation
  • 17Authentication: tokens & refreshAn interceptor that attaches the token, refreshes once on 401 and replays the queued requests — plus where you keep the token.Security & globalisation
  • 17OAuth 2.1 & OpenID Connect with PKCEThe authorization code flow with PKCE, step by step: verifier and challenge, state and nonce, the ID token, and why a BFF is safer.Security & globalisation
  • 17Internationalisation (i18n)$localize, the i18n attribute, ICU plural/select, a build per language vs runtime translations, and locale-based formatting.Security & globalisation