<link rel="stylesheet" href="assets/fonts/inter.css"> Angular Module Federation: A Step-by-Step… | Viral Patel Studio Skip to main content

Angular Module Federation: A Step-by-Step Micro-Frontends Guide

May 28, 2025By Viral Patel

Angular module federation in 2026: build a host and remote step by step with Native Federation, compare Webpack and Rspack, and know when to skip it.

Angular Module Federation: A Step-by-Step Micro-Frontends Guide

Angular module federation lets separately built and deployed Angular apps load each other's code at runtime, so one team can ship a feature without rebuilding the whole product. In 2026 the practical default for new Angular projects is Native Federation on the esbuild application builder. The Webpack plugin still works and Rspack with Module Federation 2.0 is a credible alternative. This guide builds a host and a remote step by step, then covers shared singletons, manifests, SSR and the cases where you should not do this at all.

learn about Angular Development

angular 2026

angular development

design systems

Angular Storybook (2026): Setup, Docs & Design System Guide


Eight Teams, One Release Train

A single Angular codebase that serves eight product teams has a predictable failure mode. Merge conflicts become a daily tax. A bug in checkout blocks the profile release. A hotfix in one domain forces regression testing across features nobody touched.

An Angular micro frontend splits that codebase into independently deployable slices, each owned by one team. A thin host application composes them in the browser. It is the organisational idea behind microservices, applied to the front end.

The technology has also shifted since this guide first appeared. Angular's own tooling moved to esbuild, and the federation tooling followed. Which package you install now depends on your builder, not just your Angular version. The sections below sort that out first, then build the thing.


Section 1: Pick Your Federation Stack for 2026

Four options exist. They are not interchangeable, so choose before you write config.

Option Builder Package Status I verified
Native Federation Angular @angular/build:application (esbuild) @angular-architects/native-federation Published under this name from Angular 22; -v4 suffix on Angular 20 and 21
Module Federation (Webpack) Webpack via @angular-builders/custom-webpack @angular-architects/module-federation Version ^22.0.0 targets Angular 22
Module Federation (Rspack) Rspack via @nx/angular-rspack Same plugin, rspack option Marked experimental in the plugin README
Nx wrappers Nx executors @nx/module-federation Nx documents these APIs as removed in Nx v24

Sources: the Native Federation docs, the module-federation-plugin README and Nx's migration guide.

Native Federation is the path that keeps Angular's default esbuild pipeline. The adapter wraps @angular/build:application, so CLI features such as the Vite dev server, prerendering and incremental hydration stay available. It does not use Webpack's runtime. It builds standard ES modules and an import map, and publishes a remoteEntry.json instead of a JavaScript container.

Webpack Module Federation is the original. It is mature and widely documented. The cost is that you leave the default builder. As of Angular 22 the plugin injects its config through @angular-builders/custom-webpack, and earlier versions used ngx-build-plus.

Rspack with Module Federation 2.0 is the fast-build option. Module Federation 2.0 adds a standalone runtime, a JSON manifest (mf-manifest.json) and runtime plugins, and ships through @module-federation/enhanced. Rspack also has Module Federation built in. The Angular side relies on @nx/angular-rspack.

My recommendation: start new projects on Native Federation. Stay on Webpack if you already run it in production and it is stable. Evaluate Rspack when build time is your measured bottleneck. If you are on Nx, read the removal notice before you generate anything new.


Section 2: Build a Host and Remote with Native Federation

You need an Angular workspace with two applications: shell (the host) and mfe1 (the remote). The adapter requires Angular CLI 16.1 or later.

Install and initialise

npm install -D @angular-architects/native-federation
ng add @angular-architects/native-federation --project mfe1 --port 4201 --type remote
ng add @angular-architects/native-federation --project shell --port 4200 --type dynamic-host

On Angular 20 or 21, install @angular-architects/native-federation-v4 instead and substitute the name in every command and import.

The schematic does several things per project. It renames your existing build and serve targets to esbuild and serve-original, then points build and serve at the federation builder. It adds es-module-shims to the polyfills. It splits main.ts into a federation bootstrap and a bootstrap.ts holding your original Angular bootstrap.

That split is not cosmetic. Angular itself is a shared library, so federation must initialise and negotiate versions before any Angular code runs.

Configure what the remote exposes

// projects/mfe1/federation.config.mjs
import {
  withNativeFederation,
  fromPackageJson,
} from '@angular-architects/native-federation/config';

export default withNativeFederation({
  name: 'mfe1',
  exposes: {
    './Routes': './projects/mfe1/src/app/mfe.routes.ts',
  },
  shared: fromPackageJson({
    singleton: true,
    strictVersion: true,
    requiredVersion: 'auto',
    build: 'package',
  })
    .patch(['@angular/*'], { includeSecondaries: { keepAll: true } })
    .get(),
});

The schematic generates a file very close to this, exposing ./Component by default. I changed it to expose a routes file, which scales better than exposing a single component. The exposes map is the contract: anything not listed is invisible to the host.

The remote's routes file is plain standalone Angular, with no NgModule:

// projects/mfe1/src/app/mfe.routes.ts
import { Routes } from '@angular/router';
import { FlightsComponent } from './flights.component';

export const MFE_ROUTES: Routes = [
  { path: '', component: FlightsComponent },
];

Load the remote in the host

{ "mfe1": "http://localhost:4201/remoteEntry.json" }

That is the federation.manifest.json the dynamic-host type creates in the host's public folder. The generated main.ts reads it, initialises federation, then imports the real bootstrap. Routes then use loadRemoteModule in place of a dynamic import():

// projects/shell/src/app/app.routes.ts
import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/native-federation';

export const routes: Routes = [
  {
    path: 'flights',
    loadChildren: () =>
      loadRemoteModule('mfe1', './Routes').then((m) => m.MFE_ROUTES),
  },
];

Serve both with ng serve, open the shell, and navigate to /flights. The router fetches the remote's metadata, resolves shared packages and renders the remote inside the shell's <router-outlet>. The router has no idea the code came from another deployment.

One caveat: the Native Federation docs mark this top-level loadRemoteModule import as deprecated. The recommended pattern takes the loader from the resolved initFederation promise and passes it through Angular's dependency injection. The deprecated form is shorter and works for learning. For production code, follow the Runtime page in the docs.


Section 3: Load Remotes Dynamically with a Manifest

Hardcoded remote URLs break the moment you add a staging environment. A manifest solves that. It is a JSON file mapping remote names to their entry URLs, and the host reads it at startup.

With Native Federation you already have one. The dynamic-host type reads federation.manifest.json, which the docs describe as an asset you can swap per environment without rebuilding the host:

{
  "mfe1": "https://flights.example.com/remoteEntry.json",
  "mfe2": "https://holidays.example.com/remoteEntry.json"
}

Deploying a new manifest repoints a remote, rolls back a bad release or adds a new micro frontend. Each remote's production build outputs a remoteEntry.json beside its normal bundles, so you deploy that folder as a static site and point the manifest at it.

Module Federation 2.0 takes the same idea further. @module-federation/enhanced generates an mf-manifest.json per build and exposes a runtime API that works without a build tool:

import { registerRemotes, loadRemote } from '@module-federation/enhanced/runtime';

registerRemotes([
  { name: 'mfe1', entry: 'https://flights.example.com/mf-manifest.json' },
]);

const routes = await loadRemote('mfe1/Routes');

Treat this as a sketch and confirm option names against the Module Federation docs for your version. Nx's migration guide uses registerRemotes with type: 'module' for Angular remotes, so the entry type matters.

Handle failure. Remotes will be unreachable sometimes. Without a catch, one failed remote breaks that route's navigation. Return a fallback component:

{
  path: 'flights',
  loadChildren: () =>
    loadRemoteModule('mfe1', './Routes')
      .then((m) => m.MFE_ROUTES)
      .catch(() => import('./unavailable.routes').then((m) => m.UNAVAILABLE_ROUTES)),
}

Section 4: Shared Singletons and Version Strategy

This is where most production incidents start. Angular requires exactly one copy of @angular/core per page. A second copy produces dependency injection errors such as NG0203, which the Native Federation docs describe for remotes that pull in a duplicate core.

Three options control sharing:

  • singleton: true loads one instance across all apps.
  • strictVersion: true throws when versions are incompatible, instead of silently loading two copies.
  • requiredVersion: 'auto' reads the range from your package.json.

The generated configs use these three options with shareAll or fromPackageJson. In the Native Federation Angular adapter, both helpers skip packages that should never be shared, such as zone.js and Angular testing entry points.

Share a design system as a singleton. Add your component library to shared in the host and every remote. Design tokens delivered as CSS custom properties need no federation at all: load one stylesheet in the shell and every remote inherits the variables through the cascade. For the governance side, see the design system maturity model and the multi-brand design system architecture guide.

Pin sharing to a version line. Native Federation adapters from 22.0.4 include autoShareScope(), which names a share scope after your Angular minor version, for example ng22.1. Remotes then share dependencies only with hosts on the same line. That makes a staged Angular upgrade survivable, because old and new remotes stop colliding on one singleton.

Expect to upgrade together. Federation does not let a single page run two Angular versions. Plan major upgrades as coordinated work across host and remotes, or use version-scoped sharing to isolate them during the transition.

Share less, not more. Sharing has a cost: the Webpack plugin README notes that shared packages cannot be tree-shaken. Share the framework and your design system. Leave everything else bundled in each remote.


Section 5: The Webpack Route and the Standalone Config

If your project is on Webpack Module Federation, the shape is the same but the files differ. Run the schematic with a type switch:

ng add @angular-architects/module-federation --project shell --port 4200 --type host
ng add @angular-architects/module-federation --project mfe1 --port 4201 --type remote

The remote exposes a routes file, again standalone:

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

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

The shell lazy-loads it with the same router API:

{
  path: 'flights',
  loadChildren: () =>
    loadRemoteModule({
      type: 'module',
      remoteEntry: 'http://localhost:4201/remoteEntry.js',
      exposedModule: './Routes',
    }).then((m) => m.MFE_ROUTES),
}

Two details matter. First, the plugin README advises loading remoteEntry upfront where possible so Module Federation can consider the remote's metadata when negotiating shared versions. Second, the same async-bootstrap pattern applies: a small main.ts with a dynamic import of bootstrap.ts. For a dynamic host, use --type dynamic-host and the plugin reads remote URLs from a config file at runtime.

For the Rspack variant, the plugin's ng add asks which builder you want. The README labels the Rspack path experimental, so test it in a branch before betting a roadmap on it. The Angular Architects article on Nx, Rspack and Module Federation shows loadRemote('mfe1/Component') inside a loadComponent route.

Migrating NgModule remotes. Older posts, including the first version of this one, exposed an NgModule and loaded it with .then(m => m.DashboardModule). You can still do that, but exposing routes or a standalone component is simpler and drops the module layer. If your remotes are still NgModule-based, the standalone components migration guide covers the conversion.


Section 6: SSR, Nx and Performance

Server-side rendering

Federation and SSR can coexist, but with constraints. Native Federation supports Angular SSR and incremental hydration for hosts and remotes. Four caveats are documented:

  • Launch order. Production servers start with node --import and the adapter's node-preload, so the federation loader registers before any @angular module loads. A plain node server.mjs is not enough.
  • Remotes first. The host fetches each remoteEntry.json once at startup. If a remote is down at that moment, its region renders empty until the host restarts.
  • Federated loadChildren hides routes from the build. Angular's SSR build crawls the route config statically. Child routes inside a federated loadChildren never enter the route manifest, so a direct request to a nested URL can return a 404. The docs recommend loadComponent children owned by the host.
  • Prerendering. Federated remotes load at runtime, so the schematic switches scaffolded routes from RenderMode.Prerender to RenderMode.Server.

For the Webpack plugin, the README's SSR section describes the older Angular Universal setup, and I could not verify it against the current SSR builder. If you need SSR with federation, treat Native Federation's documented path as the safer bet and read my Angular SSR and hydration guide for the rendering side.

Nx

Nx still orchestrates federated workspaces well, using its project graph for affected builds and caching. The wrappers changed though. Nx documents that the withModuleFederation helper and the Module Federation dev-server executors are removed in Nx v24. Its migration guide offers two routes: keep your bundler and use the official @module-federation/enhanced plugin, or move to Native Federation. For the workspace side, see the Nx monorepo guide for Angular.

Performance

Federation adds runtime work: extra metadata requests and the risk of duplicated dependencies. Keep it in check by sharing only what you must, lazy-loading remotes on navigation, and measuring. I do not quote overhead figures because they depend entirely on your shared-dependency setup. Set a budget and check it per remote, as in the web performance budget guide.


Section 7: When NOT to Use Micro Frontends

Micro frontends are an organisational pattern. They solve a team coordination problem, and they cost you simplicity.

Skip them when:

  • One team owns the whole front end. Independent deployment has no value when everyone releases together anyway.
  • Domains are not stable. If feature boundaries move every quarter, you will keep re-cutting remotes.
  • Heavy code sharing. If most code crosses domain lines, every change becomes a coordinated change across repos and versions.
  • SEO and first paint are critical and your team cannot own SSR. The SSR constraints above are real operational work.
  • You want it for the technology. A well-structured Nx monorepo with enforced module boundaries gives most of the ownership benefit with far less runtime machinery.

Use them when you have several teams with clear domain ownership, different release cadences and a genuine need to deploy independently. A useful test: can you name the team that owns each remote, and would that team be blocked today by someone else's release?

Cut remotes by team ownership, not by component. A team that owns checkout should ship one remote, not separate ones for cart, payment and confirmation. And remotes must never import from each other. Communicate through shell-provided services, URL parameters or browser custom events, or you will have built a distributed monolith.


Conclusion: Decide the Org Chart Before the Config

The configuration file takes an afternoon. Native Federation or Webpack, Rspack or Nx: all of them can be wired up quickly, and the sections above give you a working host and remote.

The hard part is the boundary. If you can point at a domain and say one team owns it, a federated Angular app will feel liberating. If ownership is blurry, federation amplifies the confusion and you will spend your time debugging shared-version conflicts.

Draw the team boundaries first, then pick the tool.