Native Federation
Native Federation is an open implementation of the micro frontend pattern built on native browser features — ES modules and Import Maps — rather than on a specific bundler. While the original Module Federation is tied to webpack, Native Federation works with any bundler, which makes it the natural choice for modern Angular apps that have migrated to the esbuild-based application builder. It gives you the same shell-and-remotes architecture, runtime composition, and dependency sharing, but with a tooling-agnostic foundation that won’t break when the build pipeline evolves.
Why Native Federation
When Angular moved its default builder from webpack to esbuild (the @angular/build:application builder), the classic @angular-architects/module-federation package stopped being the right fit, because Module Federation is a webpack-specific runtime. Native Federation closes that gap. It uses the EcmaScript Module standard to load remote code at runtime and Import Maps to wire shared dependencies, so the federation logic lives in the browser standard, not in the bundler.
| Aspect | Module Federation | Native Federation |
|---|---|---|
| Bundler | webpack only | Any (esbuild, Vite, Rollup, webpack) |
| Runtime mechanism | webpack chunk loading | Native ESM + Import Maps |
| Angular builder | browser (webpack) | application (esbuild) |
| Config style | webpack plugin | federation.config.js + esbuild plugin |
| Spec stability | webpack internal | Web standards |
Use Native Federation for any new Angular 17+ project. It is the only federation approach officially aligned with the esbuild application builder, and migrating later is far costlier than starting on it.
Installation and setup
Add the package and run its initializer for both the shell (host) and each remote. The schematic wires up the build target, generates a federation.config.js, and patches main.ts to bootstrap through the federation runtime.
npm install @angular-architects/native-federation -D
# Initialize the shell on port 4200
ng g @angular-architects/native-federation:init \
--project shell --port 4200 --type host
# Initialize a remote on port 4201
ng g @angular-architects/native-federation:init \
--project mfe-orders --port 4201 --type remote
Configuring a remote
Each remote exposes one or more modules and declares which dependencies it shares. The generated federation.config.js uses helper functions to auto-detect and share Angular packages from your package.json.
// projects/mfe-orders/federation.config.js
const { withNativeFederation, shareAll } = require(
'@angular-architects/native-federation/config'
);
module.exports = withNativeFederation({
name: 'mfe-orders',
exposes: {
'./Routes': './projects/mfe-orders/src/app/orders.routes.ts',
},
shared: {
...shareAll({
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
}),
},
skip: ['rxjs/ajax', 'rxjs/fetch', 'rxjs/testing', 'rxjs/webSocket'],
});
The exposed orders.routes.ts is a standard standalone route array — nothing federation-specific leaks into your feature code.
// projects/mfe-orders/src/app/orders.routes.ts
import { Routes } from '@angular/router';
export const ORDERS_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./order-list/order-list.component')
.then((m) => m.OrderListComponent),
},
{
path: ':id',
loadComponent: () =>
import('./order-detail/order-detail.component')
.then((m) => m.OrderDetailComponent),
},
];
Bootstrapping the shell
The shell initializes the federation runtime before bootstrapping Angular. initFederation() loads the federation.manifest.json, which maps remote names to their remoteEntry.json URLs. Keeping this manifest external means you can change remote locations per environment without rebuilding.
// projects/shell/src/main.ts
import { initFederation } from '@angular-architects/native-federation';
initFederation('/assets/federation.manifest.json')
.catch((err) => console.error(err))
.then(() => import('./bootstrap'))
.catch((err) => console.error(err));
// projects/shell/src/assets/federation.manifest.json
{
"mfe-orders": "http://localhost:4201/remoteEntry.json"
}
Loading a remote at runtime
Use loadRemoteModule() inside the shell’s routing config to pull the exposed module from the remote. The shell stays a thin standalone app whose routes resolve to remote code on demand.
// projects/shell/src/app/app.routes.ts
import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/native-federation';
export const routes: Routes = [
{
path: 'orders',
loadChildren: () =>
loadRemoteModule('mfe-orders', './Routes').then(
(m) => m.ORDERS_ROUTES
),
},
{
path: '',
loadComponent: () =>
import('./home/home.component').then((m) => m.HomeComponent),
},
];
When you serve both apps and navigate to /orders, the shell fetches the remote entry and mounts the remote’s routes:
Output:
NF: Loading remote mfe-orders from http://localhost:4201/remoteEntry.json
NF: Sharing @angular/[email protected] (singleton)
NF: Sharing @angular/[email protected] (singleton)
NF: Mapped './Routes' -> orders.routes-AB12CD34.js
✔ Remote mfe-orders ready
Sharing dependencies safely
Native Federation shares packages through Import Maps. Marking Angular as a singleton with strictVersion guarantees the shell and every remote use exactly one copy of the framework at runtime — critical, because two Angular instances break dependency injection and zone/signal reactivity. The requiredVersion: 'auto' setting reads the version straight from package.json so you never hand-maintain version strings.
// Reading from a shared signal-based store after federation load
import { Component, inject } from '@angular/core';
import { CartStore } from '@my-org/shared';
@Component({
selector: 'app-order-list',
standalone: true,
template: `
@if (cart.count() > 0) {
<p>You have {{ cart.count() }} item(s) in your cart.</p>
} @else {
<p>Your cart is empty.</p>
}
`,
})
export class OrderListComponent {
readonly cart = inject(CartStore);
}
Because CartStore is shared as a singleton, the signal it exposes is the same instance across the shell and all remotes, so updates propagate everywhere instantly.
Best practices
- Always set
singleton: trueandstrictVersion: truefor@angular/*packages to prevent duplicate framework instances. - Keep
federation.manifest.jsoninassets/and swap it per environment instead of hardcoding remote URLs in code. - Expose route arrays (
./Routes) rather than individual components so remotes own their own internal navigation. - Share cross-cutting state and design-system libraries as versioned singletons; never copy shared logic into each remote.
- Pin compatible Angular versions across the shell and all remotes — a singleton can only resolve one version at runtime.
- Add the remote
remoteEntry.jsonURLs to your CSP and CORS config so production browsers can fetch them.