Skip to content
Angular ng microfrontends 4 min read

Module Federation

Webpack Module Federation lets a host application load JavaScript from a completely separate build at runtime, so independently deployed micro frontends can be stitched together in the browser without rebuilding the shell. Instead of publishing components as npm packages, each remote exposes them over HTTP, and the host pulls them on demand. The @angular-architects/module-federation schematics wire all of this into the Angular CLI, including a smart way to share Angular itself so it is downloaded only once. This page focuses on Webpack-based federation; the newer esbuild-friendly approach is covered in Native Federation.

How federation works

A remote is a build that exposes one or more modules through a remoteEntry.js manifest. A host (or shell) consumes those modules by fetching the remote entry and dynamically importing the exposed path. Both sides declare a set of shared dependencies — Angular, RxJS, your design system — so the runtime can negotiate a single compatible copy rather than shipping duplicates.

The key shift versus a normal monorepo is that the host never imports the remote at compile time. The link is resolved in the browser, which means you can redeploy a remote without touching or rebuilding the shell.

Adding federation to a project

Install the plugin and run the schematic against each project. The type flag tells the schematic whether the project is the shell that loads remotes or a remote that exposes them.

npm install @angular-architects/module-federation -D

# the shell that loads remotes
ng g @angular-architects/module-federation:init \
  --project shell --type host --port 4200

# a feature served on its own origin
ng g @angular-architects/module-federation:init \
  --project checkout --type remote --port 4201

The schematic adds a webpack.config.js to each project and switches the builder to a custom Webpack builder so the federation plugin runs during ng serve and ng build.

Configuring a remote

The remote’s webpack.config.js names the remote, points at its entry file, and lists the modules it exposes. Here the checkout remote publishes a standalone component.

const { withModuleFederationPlugin, shareAll } =
  require('@angular-architects/module-federation/webpack');

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

shareAll shares every dependency in package.json. singleton: true guarantees one instance of Angular across all micro frontends — essential, because two Angular runtimes will fight over zone.js and dependency injection.

Loading a remote from the host

On the host side you do not list the remote statically. Instead you load it lazily through a route using loadRemoteModule, so the federated chunk is fetched only when the user navigates there.

import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/module-federation';

export const routes: Routes = [
  {
    path: 'checkout',
    loadComponent: () =>
      loadRemoteModule({
        type: 'module',
        remoteEntry: 'http://localhost:4201/remoteEntry.js',
        exposedModule: './Component',
      }).then((m) => m.CheckoutComponent),
  },
];

Tip: Hard-coding remoteEntry URLs is fine for a demo but brittle in production. Move them into a runtime manifest (a small JSON file fetched at startup) so the same build can point at dev, staging, and prod remotes without recompiling.

Using a runtime manifest

The plugin ships a manifest helper. Load it before bootstrap, then reference remotes by name.

import { bootstrapApplication } from '@angular/platform-browser';
import {
  initFederation,
  loadRemoteModule,
} from '@angular-architects/module-federation';
import { AppComponent } from './app/app.component';

initFederation('/assets/mf.manifest.json')
  .then(() => bootstrapApplication(AppComponent, { /* providers */ }))
  .catch((err) => console.error('Federation init failed', err));
{ "checkout": "http://localhost:4201/remoteEntry.js" }

Routes then load by manifest key instead of a raw URL:

loadRemoteModule({ remoteName: 'checkout', exposedModule: './Component' });

Sharing dependencies

Sharing is what keeps federation efficient. Each library is shared with options that control how the runtime resolves versions.

OptionEffect
singleton: trueForces one shared instance; required for Angular and RxJS.
strictVersion: trueThrows at runtime if versions are incompatible instead of silently loading two.
requiredVersion: 'auto'Reads the version from package.json so you don’t hand-maintain it.
eager: trueBundles the dep into the initial chunk instead of loading it async.

When the host and a remote request the same singleton at compatible versions, only one copy is downloaded; the second build simply reuses what is already in memory. A version mismatch on a strictVersion singleton surfaces immediately rather than as a subtle runtime bug.

Output:

[Module Federation] Sharing @angular/[email protected] (singleton)
[Module Federation] Sharing [email protected] (singleton)
✔ Loaded remote 'checkout' from http://localhost:4201/remoteEntry.js

Best Practices

  • Always mark Angular, @angular/common, RxJS, and zone.js as singleton: true — multiple runtimes break DI and change detection.
  • Keep major versions of shared libraries aligned across host and remotes; use strictVersion so mismatches fail loudly during testing.
  • Load remotes lazily through routes with loadRemoteModule so users only download the micro frontends they actually visit.
  • Resolve remote URLs from a runtime manifest instead of hard-coding them, so one artifact deploys to every environment.
  • Wrap remote loading in an error boundary or fallback route — a remote can be temporarily unavailable, and the shell should degrade gracefully.
  • Avoid sharing app-specific libraries that change often; share only stable framework and design-system packages to limit cross-team coupling.
Last updated June 14, 2026
Was this helpful?