AngularAtlased. 22.2

Ch. 13Architecture at scale57 / 75

Micro frontends

When it is worth splitting an app into several deployments and when it is just complexity: Native Federation, Module Federation, iframes, Web Components.

architecture ~0 min read

A micro frontend is a piece of UI that is built, tested and shipped independently by one team, then composed with the others at runtime, in the browser. The word that matters is “independently”: if two parts must be shipped together, they are not micro frontends, but modules of the same application — however separate their folders are.

The architecture solves an organisational problem, not a technical one: several autonomous teams that want to ship at different rhythms without blocking each other. The price is paid in runtime complexity: dependency versions, integration failures, visual consistency, testing, observability.

In short
  • Micro frontends solve an organisational problem (teams that ship independently), not a technical one.
  • The price is real: more JavaScript, dependency versions to keep in step, harder end-to-end tests.
  • For Angular with esbuild, the recommended option is Native Federation. Web Components and iframes fit when you mix frameworks or want full isolation.
  • The contracts between apps (routes, events, versions) matter more than the technology. Every remotely loaded app needs a fallback.
  • With a single team, a monorepo with clear boundaries is almost always simpler.
Why it matters

Most Angular applications, including large ones, are better served by a single application in a monorepo, with clear boundaries and lazy loading. Micro frontends are the right answer for a few precise situations, and an expensive source of problems otherwise. This page helps you tell them apart and, if needed, build them correctly with Native Federation.

What it is and what it is not

A micro frontend system has three properties, all of them mandatory:

  • Separate build — each part has its own pipeline and its own package.json (or at least its own build target).
  • Separate deploy — the Catalog team publishes a new version without the shell or the Cart team doing anything.
  • Runtime composition — a shell (host) finds out only in the browser which version of each remote to load, usually from a manifest.
BUILD-TIME — NOT a micro frontend
  libs/catalog ─┐
  libs/cart    ─┼─► ng build shell ─► one single deploy
  libs/reviews ─┘
  one version, one release, contracts checked by the compiler

RUNTIME — micro frontends
  catalog → build → deploy → catalog…/remoteEntry.json ─┐
  cart    → build → deploy → cart…/remoteEntry.json    ─┼─► the shell composes them
  reviews → build → deploy → reviews…/remoteEntry.json ─┘   in the browser
  N versions, N releases, contracts verified only at runtime
CompositionHow it worksIs it a micro frontend?
Libraries in a monorepothe shell imports libs/catalog; one build, one deployno — it is build-time (see Monorepo)
Lazy routes, @deferseparate chunks, but produced by the same buildno — the same application
Native / Module Federationremotes publish ES modules + metadata; the shell loads them from the manifest and shares dependenciesyes
Web Componentseach team ships a bundle that defines tags; the shell only uses themyes (see Angular Elements)
iframesseparate documents, inside a frameyes — the strongest isolation

Real reasons, false reasons

The reasons that justify the price are almost all about people and delivery:

  • Autonomous teams with different cadences. The checkout team ships several times a day, the catalog once a week, and a shared release blocks both: a regression in the catalog stops an urgent payment fix.
  • Very large organisations. Dozens of frontend developers on the same product, several business domains, each with clear ownership (Conway's law: the architecture ends up reflecting the organisation anyway).
  • Gradual migration (strangler fig). A new shell “wraps” the old application (AngularJS, an older Angular, another framework); screens migrate one by one, and the old code shrinks until it disappears — without a “big bang” rewrite.
  • Separate products under one experience: acquisitions, business units, portals that bring together existing applications.

False reasons sound good in presentations, but have cheaper solutions:

“We want MFE for…”Why it does not holdWhat actually solves it
scalability / performanceMFE adds bytes, requests and version negotiation at startuplazy routes, @defer, SSR, optimisations
clean, modular codemodularity is a property of the code, not of the deploylibraries + import rules (boundaries, structure)
faster buildsN pipelines mean N places that can breakincremental build and cache (Nx affected) in a monorepo
each team choosing its own frameworktechnological diversity is a permanent cost: a duplicated design system, people hard to move between teamsaccept it only when it is unavoidable (migration, acquisition)
The simple test

If you cannot name two teams that today block each other at release time, you do not have the problem that micro frontends solve.

The price

Each cost below shows up in practice; the difference between a healthy system and a fragile one is whether you have a plan for it from day one.

CostWhat concretely happensHow you keep it under control
Shared versionsa remote built with another Angular cannot use the shell's runtime; major upgrades become coordinatedthe same version everywhere, a shared upgrade calendar, a check in CI
Runtime integrationa renamed export in a remote breaks the shell in production, not at compile timeexposes treated as a public API (semver), contract tests, a fallback per remote
Duplicate bundleswhatever is not shared is downloaded N times; whatever is shared is built separately, so it is no longer tree-shaken per applicationa bundle analyser, share only what makes sense
Design systemfive teams, five versions of buttonsthe design system as a shared package + CSS tokens, owned by a platform team
Routingwho owns the URL, what happens on a direct deep link into a remotethe shell owns the top-level routes, each remote exports Routes for its prefix
Auth and sessionthe token must reach all remotes, the refresh must happen only oncean HttpOnly cookie or an auth service from a shared library as a singleton (auth refresh)
E2E testingeach team is green in isolation, the bug appears in the compositione2e on “shell + the production versions of the others”
Observability“error in production” — from which remote, which version?the version of each remote in every log, source maps per remote
Local developmentyou start five servers to work on onea manifest per environment: the local shell loads the deployed remotes, you run only what you change

Native Federation step by step

Native Federation implements the Module Federation model on web standards: remotes are ordinary ES modules, linked through an import map. For Angular 22 the package is @angular-architects/native-federation 22.x — the version follows the Angular major.minor — and it stays on the CLI's esbuild builder (@angular/build:application), which it wraps. The project has moved to the github.com/native-federation organisation. The mechanism, step by step:

  1. Build. Each remote produces ES modules and a remoteEntry.json: what it exposes (./routes → the generated file) and what dependencies it shares, with version, requiredVersion, singleton, strictVersion.
  2. Shell startup. main.ts calls initFederation() before Angular: it reads the manifest (name → URL), downloads the metadata of each remote, negotiates the versions and writes the import map (with es-module-shims).
  3. Bootstrap. Only then import('./bootstrap') starts Angular. That is why the schematic moves the bootstrap into a separate file: @angular/core must be resolved through the import map, so it has to exist first.
  4. Navigation. loadRemoteModule('catalog', './routes') is, in essence, a dynamic import() of a URL on the Catalog team's server. When the remote's code does import '@angular/core', the import map sends it to the shell's copy: one Angular, one root injector.
# the adapter's version follows the Angular major.minor (22.2.x for Angular 22.2)
npm i -D @angular-architects/native-federation

ng g @angular-architects/native-federation:init \
  --project catalog --port 4201 --type remote
ng g @angular-architects/native-federation:init \
  --project shell --port 4200 --type dynamic-host

# What the schematic changes:
#  • build/serve → @angular-architects/native-federation:build,
#    which delegates to @angular/build:application (esbuild)
#  • main.ts → initFederation(...) then import('./bootstrap');
#    the old bootstrap ends up in bootstrap.ts
#  • generates federation.config.mjs (+ public/federation.manifest.json on dynamic-host)
#  • adds es-module-shims to polyfills (import maps with scopes)
#  • with --webcomponent: bootstrap as a custom element (createApplication + define)
// catalog/federation.config.mjs — one remote, as generated by 22.x
import {
  withNativeFederation,
  fromPackageJson,
} from '@angular-architects/native-federation/config';

export default withNativeFederation({
  name: 'catalog',

  exposes: {
    // the remote's public API
    './routes': './projects/catalog/src/app/catalog.routes.ts',
  },

  shared: fromPackageJson({
    singleton: true,
    strictVersion: true,
    requiredVersion: 'auto',
    build: 'package',
  })
    // all of @angular/core, including the secondary entry points → a single copy
    .patch(['@angular/core'], { includeSecondaries: { keepAll: true } }),

  skip: ['rxjs/ajax', 'rxjs/fetch', 'rxjs/testing', 'rxjs/webSocket'],

  features: { denseChunking: true },
});
// shell/src/main.ts — runs BEFORE Angular:
// reads the manifest, downloads the remoteEntry.json files, negotiates the versions,
// writes the import map — only then loads bootstrap.ts (and @angular/core)
import { initFederation } from '@angular-architects/native-federation';

initFederation('federation.manifest.json')
  .then((nf) => import('./bootstrap').then((m) => m.bootstrap(nf)))
  .catch((err) => console.error(err));
What changed in 22.x
  • For Angular 22, Native Federation v4 comes back into the main package @angular-architects/native-federation (for Angular 20–21 there was the -v4 bridge).
  • The schematic generates federation.config.mjs with fromPackageJson(...); shareAll and share remain available.
  • loadRemoteModule imported directly from the package is marked @deprecated: use the one from the result of initFederation(), as in app.routes.ts.
// The short form from older tutorials still exists in 22.x,
// but it is marked @deprecated:
import { loadRemoteModule } from '@angular-architects/native-federation';

const route = {
  path: 'catalog',
  loadChildren: () =>
    loadRemoteModule('catalog', './routes').then((m) => m.CATALOG_ROUTES),
};

// The reason, from the d.ts: it depends on a global instance and on the LAST initFederation,
// so it is fragile in tests and with several hosts.
// Recommended: loadRemoteModule from the result of initFederation (see app.routes.ts).

The demo below reproduces exactly this flow. The servers are simulated, but each “remote” is a real Angular component, sitting in its own chunk and loaded with a dynamic import() — the same mechanism that loadRemoteModule uses underneath. The catalog and the cart talk only through events on window; the reviews do not talk to anyone.

LiveShell + three remotes (simulation, with real lazy loading)
federation.manifest.json (editable)
Deployments (simulated servers)
catalog.atlas.example
cart.atlas.example
reviews.atlas.example
shell@angular/core 22.2.2cart: —
<slot catalog>idle
<slot cart>idle
<slot reviews>idle
What the shell does
Put reviews on “offline”, put cart on @angular/core 21.2.4 or break a URL in the manifest, then press “Load the page”: each slot fails separately, with a fallback and “Retry”. “slow (4 s)” exceeds the shell's timeout. Add from the catalog: the shell's badge updates through an event. In DevTools → Network you see the real chunks of the remotes.

Shared dependencies and versions

For each package in shared, a remote declares four things: version (what it was built with), requiredVersion (what it accepts; 'auto' = the range from package.json, for example ^22.2.0), singleton (a single instance for everyone) and strictVersion (refuses a version outside the range).

Why Angular must be a singleton. @angular/core is not a library of pure functions: it is the runtime that renders, the injector, change detection. Compiled templates call the runtime instructions of the build they were compiled with, so a component from one copy cannot be rendered by another. Even for an ordinary library, two copies mean two different classes: inject(AuthService) in the remote asks for a different token than the one registered by the shell, and your “singleton” suddenly has two instances.

What happens when the versions do not match depends on the tool:

Situation (singleton: true)Native Federation (orchestrator v4)Module Federation (webpack)
the shared version satisfies requiredVersionthe remote uses the shared copy; the host's version takes priorityuses the singleton
does not satisfy, strictVersion: truethe remote gets its own copy, in the scopes of the import maperror on load: Unsatisfied version … of shared singleton module …
does not satisfy, strictVersion: falseuses the shared version anyway + a warninguses the singleton + console.warn

For Angular, “its own copy” does not save the situation: the remote would have a different runtime, so its components cannot enter the shell's tree. A remote on another major is integrated only in isolation — as a Web Component with its own bootstrap, or in an iframe. If you prefer to find out at startup, not at navigation, the orchestrator has the option strict.strictExternalCompatibility, which throws an error at initialisation instead of creating scopes.

// Three ways to write `shared` in federation.config.mjs

// 1. the default in 22.x: everything in dependencies from package.json, the same rules
shared: fromPackageJson({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),

// 2. the classic style (still exported)
shared: { ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }) },

// 3. explicit, package by package
shared: share({
  '@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
  // has state → singleton
  '@ngrx/signals': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
  // stateless utility: each remote can have its own copy
  'date-fns': { singleton: false },
}),

// requiredVersion: 'auto' → the range from package.json (e.g. "^22.2.0")
// autoShareScope({ level: 'minor' }) → shares only between remotes
//   on the same version line (e.g. "ng22.2")
LiveVersion negotiation and the import map (simulation)

The shell (host) provides @angular/core at version 22.2.2, which takes priority in the global scope.

Remotebuilt withrequiredVersionsingletonstrictVersion
catalog
cart
reviews
catalogshare
22.2.2 satisfies ^22.2.2 → uses the shell's copy, downloads nothing extra.
cartscope
22.2.2 does not satisfy ~22.1.4; with strictVersion it gets its own copy 22.1.4, visible only in its scope. Two Angular runtimes: its components cannot render in the shell's tree — integrate it in isolation (Web Component with its own bootstrap).
reviewsskip + warning
22.2.2 does not satisfy ^21.2.4, but strictVersion: false → uses 22.2.2 anyway, with a warning in the console. It may work, or crash at runtime — you find out in production.
Copies of @angular/core downloaded: 2one extra Angular runtime
generated import map
{
  "imports": {
    "@angular/core": "https://shell.example/core-22.2.2.js"
  },
  "scopes": {
    "https://cart.example/": {
      "@angular/core": "https://cart.example/core-22.1.4.js"
    }
  }
}
Simulates the Native Federation resolver rules for @angular/core. Change the version, the range and the flags of each remote, and follow the verdict and the import map. Notice how ~ (accepts only patches) creates an extra copy for no real reason, and how singleton: false always creates a second Angular.
  • Ideal: the same exact Angular version in the shell and in all remotes. ^ gives you tolerance between minors, but AOT-compiled code uses internal runtime instructions — test the combination, do not rely on semver.
  • The host wins, so on a new minor the shell can go first, as long as the remotes' ranges accept it. A new major requires a synchronised upgrade, or isolating the remote that has fallen behind.
  • Singleton + strictVersion for the framework and for everything with state: Angular, router, stores, design system, the auth library. Stateless utilities can stay non-singleton.
  • autoShareScope() ties sharing to a version line (for example ng22.2), so you do not accidentally mix incompatible builds.

Module Federation, Web Components, iframes

Module Federation (webpack)

@angular-architects/module-federation has a version for Angular 22, but it is the “classic” variant, on webpack: ng add moves the project from the esbuild builder to @angular-builders/custom-webpack (from Angular 22; up to 21 it used ngx-build-plus), and it explicitly warns you that for new projects the alternative is Native Federation. There is also an rspack builder, marked experimental. Choose it only if you are extending an existing Module Federation system or must share the runtime with remotes already built on webpack.

// Classic Module Federation (webpack) — @angular-architects/module-federation 22.x
const {
  shareAll,
  withModuleFederationPlugin,
} = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({
  name: 'catalog',
  exposes: {
    './routes': './projects/catalog/src/app/catalog.routes.ts',
  },
  shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
  },
});

Web Components

Each remote starts its own application with createApplication() and defines a tag; the shell just puts it on the page. It works across different Angular majors and across frameworks, because nothing is shared. That is exactly its cost: each bundle brings its own runtime (≈ 33 kB transferred for a simple element, measured on the Angular Elements page), there is no common DI, and communication goes through attributes and events. It combines well with federation: a remote on another major is exposed as a Web Component and still loaded through loadRemoteModule.

// A remote on ANOTHER Angular major (or another framework): integrated in isolation,
// as a Web Component with its own runtime.
//
// remote — federation.config.mjs:  exposes: { './Bootstrap': './src/bootstrap.ts' }
// remote — bootstrap.ts:
//   createApplication(appConfig).then(({ injector }) =>
//     customElements.define('mfe-legacy', createCustomElement(App, { injector })));
//
// shell — a page that loads the bundle (its effect: define), then uses the tag
import { CUSTOM_ELEMENTS_SCHEMA, Component, inject, input, resource } from '@angular/core';
import { NATIVE_FEDERATION } from '../app.config';

@Component({
  selector: 'app-legacy-page',
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    @if (bundle.hasValue()) {
      <mfe-legacy [attr.customer-id]="customerId()"></mfe-legacy>
    } @else if (bundle.error()) {
      <p role="alert">
        The legacy module is not available.
        <button (click)="bundle.reload()">Retry</button>
      </p>
    }
  `,
})
export class LegacyPage {
  private readonly nf = inject(NATIVE_FEDERATION);
  readonly customerId = input.required<string>();

  protected readonly bundle = resource({
    loader: () => this.nf.loadRemoteModule('legacy', './Bootstrap'),
  });
}

iframes

The strongest isolation: another document, another JavaScript, another CSS; errors, memory leaks and styles stay inside the frame, and with sandbox you can run code you do not trust. You pay with integration: the height is synchronised through postMessage, a dialog cannot leave the frame, deep links and browser history must be linked by hand, focus and accessibility become complicated, and each frame loads the whole application. Suitable for third-party code, legacy applications you cannot modify, and internal tools.

<!-- The strongest isolation: another document, another JS, another CSS;
     errors stay inside the frame -->
<iframe
  src="https://billing.exemplu.ro/embed?customer=42"
  title="Billing"
  sandbox="allow-scripts allow-same-origin allow-forms"
  loading="lazy"
  style="width: 100%; border: 0"
></iframe>

<!-- The price: the height is communicated through postMessage, a dialog cannot leave
     the frame, deep links and browser history are synced by hand,
     and each frame loads the whole application. -->

Contracts, communication, fallback

The contract of a remote is everything that depends on it from outside: the keys in exposes, what they export, the route parameters they read, the events they emit and the shared dependencies. Treat it as a public API with semver: an incompatible change means a new key (./routes-v2), and the old one stays until the shell migrates.

Communication, in the order it is worth trying:

  1. The URL — route parameters and query params. Free, deep-linkable, survives a refresh, and the remote does not import anything from another remote.
  2. Typed events on window, from a tiny contracts package (types + emit/listen). Do not forget the one that starts late: it has to ask for the state.
  3. A few small services, shared as singletons — auth, the current user, feature flags — from a library included in shared.

What to avoid: a common global store (the same NgRx store, or the same service holding all the state, used by every remote). Any change in the shape of the state becomes a coordinated change between teams — exactly the coupling you wanted to remove. Each remote keeps its own state; only events and URLs travel between them (see also state management).

// 1. The URL is the first channel: free, deep-linkable, survives a refresh.
//    /catalog/audio?sort=price → the remote reads from the URL, not from a shared store.
//    (withComponentInputBinding() goes in the SHELL: the remote's routes
//    run in the shell's router)
export class ProductList {
  readonly category = input<string>();                 // from :category
  readonly sort = input<'price' | 'rating'>('rating'); // from ?sort=
}

// 2. Navigation between remotes also goes through the URL:
//    the remote does not import anything from another remote.
this.router.navigate(['/cart'], { queryParams: { added: product.id } });

Fallback. Any remote can be missing: a failed deploy, a CDN that is down, a mistake in the manifest of an environment. Two situations, two solutions:

  • A whole route: let loadChildren fail and turn the error into a redirect with withNavigationErrorHandler. A rejected loader is not cached by the router, so the next navigation retries the remote.
  • A remote inside a page (a widget, a slot): a shell component loads the remote with resource, with a timeout, a fallback and reload() for “Retry” — as in the first demo.
// A FAILED remote ROUTE: let loadChildren fail and redirect.
// A rejected loader is NOT cached by the router → the next navigation retries.
import { ApplicationConfig, inject } from '@angular/core';
import {
  NavigationError,
  RedirectCommand,
  Router,
  provideRouter,
  withNavigationErrorHandler,
} from '@angular/router';
import type { NativeFederationResult } from '@angular-architects/native-federation';
import { routes } from './app.routes';

export const appConfig = (nf: NativeFederationResult): ApplicationConfig => ({
  providers: [
    provideRouter(
      routes(nf),
      withNavigationErrorHandler((e: NavigationError) => {
        const router = inject(Router);
        const target = `/indisponibil?url=${encodeURIComponent(e.url)}`;
        return new RedirectCommand(router.parseUrl(target));
      }),
    ),
  ],
});

// ✗ The trap: a fallback returned from the loader is a SUCCESS for the router.
//   It is cached on the route until the page reloads, so "Retry"
//   never reaches the remote again:
//   loadComponent: () => nf.loadRemoteModule(...).then(...).catch(() => RemoteDown)

When YES, when NO

The decision depends on the organisation more than on the code. The score below applies the rules from this page: it is not a magic formula, but it forces you to answer the questions that matter and shows you why a recommendation comes out.

LiveDo we need micro frontends?
Organisation maturity
Special situations
You probably don't need micro frontends: Monorepo + lazy routes.
Monorepo + lazy routes (one application)85
  • + 3 teams: boundaries in the code (libraries, import rules) usually suffice
  • + a shared release is acceptable
Native Federation (Angular remotes, shared runtime)0
  • + from ~4 autonomous teams, autonomy starts to matter
  • − without a real need for independent deploys, MFE does not justify its price
  • + the same Angular version → @angular/core can be a shared singleton
  • − no platform, per-team pipelines or monitoring: the complexity of MFE reaches production
Web Components (Angular Elements, runtime per bundle)0
iframes (full isolation)0
Start from your real situation. Notice how much independent deploy and organisational maturity weigh, how “several Angular majors” shifts the recommendation towards Web Components, and how untrusted code requires an iframe.
Micro frontends
  • Several autonomous teams (roughly 4–5 or more) on the same product, with different release cadences, who today block each other.
  • Independent deploy is mandatory (contractual, regulatory, different rhythms) → Native Federation with the same Angular version.
  • Gradual migration from an old application → a new shell + strangler fig; old screens as Web Components or iframes until they disappear.
  • Separate products under the same brand, with clear ownership by business domain.
  • You already have the foundation: a platform team for the shell and the design system, CI/CD per team, monitoring by remote version.
Something else
  • One team or a few teams that can ship together → one application in a monorepo, with lazy routes and boundaries.
  • The problem is performance → @defer, lazy loading, SSR; MFE makes it worse.
  • The problem is tangled code → libraries, import rules, architecture; MFE only moves the tangle between repositories.
  • You just want to reuse components between applications → a published library, or one shared inside the monorepo.
  • A widget for foreign pages → a single Web Component (Angular Elements), not an architecture.
  • There is no one to own the shell, the contracts and the common upgrades → postpone until there is.

Common pitfalls

  • The “shared” library that is not in shared → each remote bundles it separately, so providedIn: 'root' produces two instances (two classes, two tokens). Include the stateful libraries in shared and check remoteEntry.json.
  • singleton: false on @angular/core or a remote on another major → two Angular runtimes. The same version everywhere; for exceptions, isolation (Web Component, iframe).
  • strictVersion: false so that it “passes” → the message disappears from the console, the problem remains and shows up in production. Fix the versions, not the flag.
  • A fallback returned from loadComponent (.catch(() => RemoteDown)) → the router caches it on the route until a refresh, and “Retry” never reaches the remote. Let the loader fail, or use a host component with reload().
  • No timeout → a remoteEntry.json that does not respond keeps the page in “loading” forever. Put a timeout on every remote load.
  • One manifest for all environments → staging loads the production remotes (or the other way round). A manifest per environment, generated at deploy.
  • A common global store between remotes → lockstep releases, a distributed monolith. Local state + events + URL.
  • A remote that imports code directly from another remote → a hidden dependency between deploys. Everything goes through libs/contracts or through the URL.
  • Absolute routes written in a remote (/products/42) → they break when the shell mounts it under another prefix. Navigate relative to the active route.
  • Global CSS from remotes → one team's styles break another's. No global styles in remotes; CSS tokens from the design system; ShadowDom for Web Components.
  • Errors without context → you do not know which remote, in which version, crashed. Attach the remote's name and version to every reported error (see error handling).

Cheat sheet

// REMOTE — federation.config.mjs
withNativeFederation({
  name: 'catalog',
  exposes: { './routes': './src/app/catalog.routes.ts' }, // public API, versioned
  shared: fromPackageJson({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
});

// SHELL — public/federation.manifest.json (one per environment: dev / staging / prod)
// { "catalog": "https://catalog.exemplu.ro/remoteEntry.json" }

// SHELL — main.ts
initFederation('federation.manifest.json')
  .then((nf) => import('./bootstrap').then((m) => m.bootstrap(nf)));

// SHELL — routes
const catalog = {
  path: 'catalog',
  loadChildren: () =>
    nf.loadRemoteModule<{ CATALOG_ROUTES: Routes }>('catalog', './routes')
      .then((m) => m.CATALOG_ROUTES),
};

// VERSIONS: the same version of Angular everywhere; the host takes priority.
//   incompatible + strictVersion    → own copy in scopes (NF)
//                                     "Unsatisfied version …" (webpack MF)
//   incompatible, without strictVersion → uses the shared version + warning
// COMMUNICATION: URL → typed events (libs/contracts) → small shared services.
//   No common stores.
// FAILURE: route → withNavigationErrorHandler + redirect
//          slot → host component with timeout + resource.reload()
// ANOTHER MAJOR / ANOTHER FRAMEWORK → Web Component with its own runtime, or iframe

Ch. 13 · 0 of 4 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. Micro frontends
  2. Web Components with Angular Elements
  3. Monorepo, libraries & boundaries
  4. Hexagonal architecture (ports & adapters)
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