Micro Frontends with Angular 21: A Step-by-Step Shell + Remotes Build (Native Federation)
Demo: https://angular21mfe.ihaveverygoodwebsite.com/login
Source code: https://github.com/skyairwater/angular21-mfe-app
This post walks through a working Angular 21 micro-frontend setup using @angular-architects/native-federation. You’ll build a Shell (host) and three Remotes (merchant, credit, wealth), wire routing, add login + role access, and share state via Angular signals.
Remotes own: domain UI (merchant/credit/wealth) and can have their own internal routes.
Native Federation enables: shell downloads remote UI at runtime, only when users navigate.
1) What we’re building
We will create one Angular workspace containing multiple Angular apps (a micro-repo style layout):
projects/
shell/ (host)
merchant/ (remote)
credit/ (remote)
wealth/ (remote)
The shell will route to /merchant, /credit, /wealth, and dynamically load each remote using runtime federation.
What this means:
-
All projects share:
-
Node modules
-
Angular version
-
Tooling
-
-
But at runtime:
-
Each app is built and served independently
-
Each app can be deployed independently
-
This is why it’s called micro-repo, not mono-repo.
2) Create the workspace and apps
Start with an Angular workspace and generate the apps.
# Create the workspace (shell app)
ng new mfe-workspace --routing --style=css
cd mfe-workspace
# Create remote apps inside /projects
ng generate application shell --routing --style=css
ng generate application merchant --routing --style=css
ng generate application credit --routing --style=css
ng generate application wealth --routing --style=css
3) Install Native Federation dependencies
Your project uses @angular-architects/native-federation plus a couple of runtime helpers.
npm install @angular-architects/native-federation @softarc/native-federation-node es-module-shims
4) Federation config for each app (exact pattern)
Each remote exposes its root component at ./Component, pointing to its app root file: ./projects/<remote>/src/app/app.ts. This is exactly how your repo does it.
Remote example: projects/merchant/federation.config.js
const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
name: 'merchant',
exposes: {
'./Component': './projects/merchant/src/app/app.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
skip: ['rxjs/ajax','rxjs/fetch','rxjs/testing','rxjs/webSocket'],
features: { ignoreUnusedDeps: true }
});
Repeat the same for credit and wealth (same structure, just change the name and path).
Host: projects/shell/federation.config.js
const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
name: 'shell',
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
skip: ['rxjs/ajax','rxjs/fetch','rxjs/testing','rxjs/webSocket'],
features: { ignoreUnusedDeps: false }
});
5) The federation manifest (shell side)
Your shell initializes federation using a manifest file. In your project it lives here: projects/shell/public/federation.manifest.json
{
"merchant": "http://localhost:4201/remoteEntry.json",
"credit": "http://localhost:4202/remoteEntry.json",
"wealth": "http://localhost:4203/remoteEntry.json"
}
This tells the shell where to find each remote entry at runtime. In production, these URLs would typically point to a CDN path instead of localhost.
6) Bootstrapping with federation (main.ts + bootstrap.ts)
Shell: projects/shell/src/main.ts (exact)
import { initFederation } from '@angular-architects/native-federation';
initFederation('federation.manifest.json')
.catch(err => console.error(err))
.then(_ => import('./bootstrap'))
.catch(err => console.error(err));
Remotes: projects/merchant/src/main.ts (exact)
import { initFederation } from '@angular-architects/native-federation';
initFederation()
.catch(err => console.error(err))
.then(_ => import('./bootstrap'))
.catch(err => console.error(err));
All apps: src/bootstrap.ts (exact pattern)
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));
7) Shell routing: load remotes on navigation (exact)
Your shell routes are defined in projects/shell/src/app/app.routes.ts. This file does three things:
- Redirects default route to a module
- Provides a /login route
- Loads each remote with loadRemoteModule(remoteName, exposedPath)
import { Routes } from '@angular/router';
import { loadRemoteModule } from '@angular-architects/native-federation';
import { authGuard } from './core/guards/auth.guard';
import { LoginComponent } from './features/login/login.component';
export const routes: Routes = [
{ path: '', redirectTo: 'merchant', pathMatch: 'full' },
{ path: 'login', component: LoginComponent },
{
path: 'merchant',
loadComponent: () =>
loadRemoteModule('merchant', './Component').then((m) => m.App),
canActivate: [authGuard],
data: { role: 'merchant' }
},
{
path: 'credit',
loadComponent: () =>
loadRemoteModule('credit', './Component').then((m) => m.App),
canActivate: [authGuard],
data: { role: 'credit' }
},
{
path: 'wealth',
loadComponent: () =>
loadRemoteModule('wealth', './Component').then((m) => m.App),
canActivate: [authGuard],
data: { role: 'wealth' }
}
];
8) Authentication + Roles (exact implementation)
Auth service (Signals-based state): core/auth/auth.service.ts
Your project uses Angular signals for state:
import { Injectable, signal, computed } from '@angular/core';
import { Router } from '@angular/router';
export type Role = 'merchant' | 'credit' | 'wealth' | 'admin';
export interface User {
username: string;
role: Role;
name: string;
}
@Injectable({ providedIn: 'root' })
export class AuthService {
private readonly USERS: Record<string, User> = {
'merchantuser': { username: 'merchantuser', role: 'merchant', name: 'Merchant User' },
'credituser': { username: 'credituser', role: 'credit', name: 'Credit User' },
'wealthuser': { username: 'wealthuser', role: 'wealth', name: 'Wealth User' },
'admin': { username: 'admin', role: 'admin', name: 'Administrator' }
};
private readonly PASSWORD = 'abcd123';
private _currentUser = signal<User | null>(null);
currentUser = this._currentUser.asReadonly();
isLoggedIn = computed(() => !!this._currentUser());
constructor(private router: Router) {}
login(username: string, password: string): boolean {
if (password !== this.PASSWORD) return false;
const user = this.USERS[username];
if (!user) return false;
this._currentUser.set(user);
this.redirectAfterLogin(user.role);
return true;
}
logout(): void {
this._currentUser.set(null);
this.router.navigate(['/login']);
}
hasAccess(requiredRole: Role): boolean {
const user = this._currentUser();
if (!user) return false;
if (user.role === 'admin') return true;
return user.role === requiredRole;
}
private redirectAfterLogin(role: Role): void {
this.router.navigate([role === 'admin' ? '/merchant' : '/' + role]);
}
}
Route guard: core/guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from '../auth/auth.service';
export const authGuard: CanActivateFn = (route) => {
const authService = inject(AuthService);
const router = inject(Router);
if (!authService.isLoggedIn()) {
return router.createUrlTree(['/login']);
}
const requiredRole = route.data['role'];
if (requiredRole && !authService.hasAccess(requiredRole)) {
alert('You do not have access to this module.');
return false;
}
return true;
};
Login component: features/login/login.component.ts
This is a simple username/password login UI calling the auth service.
// username examples: merchantuser / credituser / wealthuser / admin
// password: abcd123
9) Shell navigation (role-aware)
The shell shows navigation only when logged in, and it displays links based on role access. This is in projects/shell/src/app/app.html.
<nav *ngIf="authService.isLoggedIn()">
<a *ngIf="authService.hasAccess('merchant')" routerLink="/merchant">Merchant</a>
<a *ngIf="authService.hasAccess('credit')" routerLink="/credit">Credit</a>
<a *ngIf="authService.hasAccess('wealth')" routerLink="/wealth">Wealth</a>
...
</nav>
10) Remotes: what they expose and what they render
Each remote exposes its root component (App) from projects/<remote>/src/app/app.ts. Your remotes render simple UI blocks (dashed border + label) so it’s obvious they’re loaded remotely.
// projects/merchant/src/app/app.html (similar for credit/wealth)
<div style="border: 2px dashed red; padding: 20px;">
<h1>Merchant App</h1>
<p>This is the Merchant micro-frontend loaded remotely.</p>
</div>
11) angular.json and ports (how local dev works)
In your setup, each app has a federation build/serve target, and each app also has a “serve-original” dev server entry with a fixed port:
shell -> 4200
merchant -> 4201
credit -> 4202
wealth -> 4203
12) Run it locally (exact workflow)
Open 4 terminals and run:
# Terminal 1
ng serve shell
# Terminal 2
ng serve merchant
# Terminal 3
ng serve credit
# Terminal 4
ng serve wealth
Then open: http://localhost:4200
You’ll be redirected to login. Use:
Users:
merchantuser (role: merchant)
credituser (role: credit)
wealthuser (role: wealth)
admin (role: admin)
Password:
abcd123
After login, try navigating to different modules. Only allowed modules will show in the navbar and pass the route guard.
13) Next enhancements (easy, real-world improvements)
- Add internal routes inside remotes (e.g., merchant/dashboard, merchant/reports)
- Add a shared library for types/contracts (UserContext, Role)
- Replace hardcoded login with a real backend token API
- Add a shared “global notification” signal in shell that remotes can trigger via a small shared contract
- Deploy: host remotes on CDN and update the shell manifest URLs
Roles of Each Application
1️⃣ Shell Application (Host)
This is the entry point.
Responsibilities:
-
Runs on
localhost:4200 -
Owns:
-
Top navigation
-
Layout
-
Global routing
-
-
Decides WHEN and WHERE to load MFEs
Important:
The shell does NOT contain business features.
Think of it like:
“I don’t know how merchant works, I only know where to load it.”
2️⃣ Merchant / Credit / Wealth (Remotes)
These are independent Angular apps.
Responsibilities:
-
Own their own:
-
UI
-
Routing
-
State
-
-
Expose one or more entry components to the shell
Important:
They do NOT know about the shell.
They just say:
“If someone wants me, this is what I expose.”
🔌 How These Apps Talk (Conceptually)
They do NOT import each other at build time.
Instead:
-
Webpack Module Federation loads code at runtime
-
The shell:
-
Knows where the remote lives
-
Knows what the remote exposes
-
-
The remote:
-
Exposes a component/module
-
Doesn’t care who consumes it
-
This loose coupling is the entire point of MFE.
🚦 Runtime Flow (Very Important)
When you open:
What happens behind the scenes:
-
Browser loads shell
-
Angular router matches
/merchant -
Shell router says:
“This route belongs to Merchant MFE”
-
Webpack:
-
Downloads merchant’s remote bundle
-
Initializes it
-
-
Merchant’s exposed component renders inside the shell
At no point is merchant bundled into shell.
🧠 Why Angular 21 Changes Everything
Angular 21 is:
-
Standalone-first
-
ESBuild-based
-
Strict builder validation
This means:
-
No
NgModuledeclarations for root components -
No legacy Webpack hacks
-
No random custom configs allowed
That’s why older tutorials caused:
-
Schema errors
-
Builder incompatibilities
-
Webpack hook failures
Your working setup respects Angular 21 rules — that’s why it runs clean.
🧭 STEP 2 — Routing in Micro Frontends (Angular 21)
1️⃣ Who owns routing?
✅ The Shell owns global routing
The shell decides:
-
/merchant -
/wealth -
/capital -
/consumer
Think of shell routing as traffic control 🚦.
“When the URL changes, which application should be activated?”
❌ MFEs do NOT know global routes
Each MFE:
-
Knows only its internal routes
-
Does NOT care whether it is loaded at
/merchantor/something-else
This is critical for independence.
2️⃣ Two Levels of Routing (Very Important)
🟦 Level 1 — Shell Routing
Example (conceptual):
The shell:
-
Matches the route
-
Dynamically loads the remote
-
Mounts the exposed component
At this point, the shell’s job is done.
🟩 Level 2 — Remote (MFE) Routing
Once loaded:
Now:
-
Merchant MFE router takes over
-
Shell is out of the picture
This is called nested routing.
3️⃣ How Shell Loads an MFE (Conceptually)
The shell route says:
“For this path, lazy-load a remote component.”
This is NOT:
-
importat build time -
Static dependency
Instead:
-
The remote is fetched over the network
-
Loaded only when needed
That’s why MFEs improve:
-
Initial load time
-
Team independence
4️⃣Standalone Components Change Routing
In Angular 21:
-
Components are standalone
-
Routes load components, not modules
So instead of:
“Load MerchantModule”
It’s:
“Load MerchantAppComponent”
This:
-
Simplifies routing
-
Reduces boilerplate
-
Fits MFE architecture perfectly
6️⃣ Security & Role-Based Routing (Conceptual)
Yes — everything still works like a normal app:
-
Auth guards → live in shell
-
Role checks → done before loading MFE
-
Unauthorized users:
-
Never download the remote
-
Never see its UI
-
This is actually more secure than monoliths.
STEP 3 — Module Federation (Angular 21, MFE)
1️⃣ What is Module Federation?
Module Federation is a Webpack feature that allows:
-
One app (Shell) to load code from another app (Remote) at runtime
-
Without rebuilding or redeploying the shell
Think of it like:
“Importing JavaScript from another server dynamically.”
But safely, versioned, and controlled.
2️⃣ Roles in Module Federation
🟦 Shell (Host)
The shell:
-
Knows where the remotes live (URLs)
-
Decides when to load them
-
Does not bundle remote code at build time
Shell responsibility:
“I orchestrate, I don’t own features.”
🟩 Remote (MFE)
Each MFE:
-
Exposes one or more entry points
-
Builds independently
-
Is deployable without touching the shell
Remote responsibility:
“I expose capabilities, not my internals.”
3️⃣ Runtime Loading (The Key Concept)
Traditional Angular:
-
Everything bundled at build time
-
One deployment = everything
With Module Federation:
-
Shell loads a manifest
-
Downloads remote bundles only when route is activated
-
Executes remote code inside the shell
This is why:
-
/merchantloads merchant code -
/wealthdoes NOT
4️⃣ Why Angular 21 Works So Well with MFEs
Angular 21:
-
Standalone components
-
No NgModule requirement
-
Cleaner dependency boundaries
Result:
-
Remotes expose a single root component
-
Shell consumes it directly
-
No complex module wiring
This removed 80% of historical MFE pain.
5️⃣ Shared Dependencies (Very Important)
Module Federation allows shared libraries:
-
Angular core
-
Router
-
RxJS
-
Design system libraries
Why this matters:
-
Prevents multiple Angular runtimes
-
Reduces bundle size
-
Avoids runtime conflicts
Interview phrase:
“Shared dependencies are singletons, ensuring only one Angular runtime is active.”
6️⃣ Version Safety (Why It Doesn’t Break)
Module Federation:
-
Negotiates versions at runtime
-
Uses compatible versions
-
Fails fast if incompatible
This avoids:
-
Random crashes
-
Silent failures
-
Dependency hell
Q: Is Module Federation Angular-specific?
👉 No. It’s a Webpack feature used by React, Vue, etc.
Q: How are remotes versioned?
👉 Independently deployed; shell loads by URL.
Q: What if a remote is down?
👉 Shell can show fallback UI or error boundary.
🧠 angular.json — What It Is & Why It Matters (Angular 21 + MFE)
1️⃣ What is angular.json?
angular.json is not configuration for Angular code — it is configuration for the Angular CLI.
Think of it as:
“How should the CLI build, serve, test, and package each application?”
Angular itself doesn’t read this file at runtime.
Only the CLI + builders do.
2️⃣ Why angular.json Became Strict in Angular 21
Modern Angular:
-
Uses schema-validated builders
-
Rejects unknown or legacy properties
-
Enforces clear boundaries
This is why you kept seeing errors like:
-
must have required property -
must NOT have additional properties
This is intentional — Angular wants:
-
Predictable builds
-
Faster tooling
-
Fewer “magic” Webpack hacks
3️⃣ Workspace vs Projects (Big Picture)
In an MFE setup, angular.json describes multiple applications.
Conceptually:
-
One workspace
-
Many projects
-
Shell (host)
-
Multiple MFEs (remotes)
-
Each project is fully independent in how it:
-
Builds
-
Serves
-
Outputs bundles
This is how Angular supports multi-app architectures.
4️⃣ What a “Project” Represents
Each project entry defines:
-
Is this an application or library?
-
Where is its source code?
-
How do we build it?
-
How do we run it locally?
In MFE terms:
-
Shell = one project
-
Each MFE = one project
They do not share build pipelines.
5️⃣ Builders (This Is the Most Important Concept)
A builder answers one question:
“How should this task be executed?”
Examples of tasks:
-
build
-
serve
-
test
Angular 21 builders are:
-
Strict
-
Schema-validated
-
Optimized for esbuild
Why this mattered for you:
-
Some builders do not allow Webpack hooks
-
Some builders require specific properties
-
Mixing old + new builders causes failures
This explains 90% of your earlier pain.
7️⃣ How MFEs Use angular.json
In a micro frontend setup:
-
Shell:
-
Knows how to build itself
-
Knows how to serve itself
-
-
MFEs:
-
Build independently
-
Are served on different ports
-
Are deployed separately
-
angular.json ensures:
-
No accidental coupling
-
No shared build artifacts
-
Clean runtime federation
🔐 Authentication in Micro Frontend Architecture (Angular 21)
1️⃣ The Core Problem Auth Solves in MFE
In a monolithic SPA:
-
One login
-
One token
-
One router
-
One app owns everything
In micro frontends:
-
Multiple apps
-
Loaded dynamically
-
Possibly deployed separately
-
Potentially owned by different teams
So the real question is:
Who owns authentication and who trusts whom?
2️⃣ The Golden Rule (Memorize This)
Authentication is centralized. Authorization is distributed.
This one sentence explains everything.
3️⃣ Who Owns Authentication?
✅ The Shell (Host Application)
The shell is responsible for:
-
Login screen
-
Credential validation
-
Token acquisition
-
Token storage
MFEs:
-
Never perform login
-
Never store credentials
-
Never refresh tokens
This avoids:
-
Duplicate login flows
-
Security holes
-
Token desync issues
4️⃣ What Does the Shell Actually Store?
Typically:
-
Access token (JWT)
-
User profile
-
Roles / claims
Stored in:
-
Memory (preferred)
-
Or session storage (acceptable)
-
Avoid localStorage in real banking apps
The shell becomes the security boundary.
5️⃣ How MFEs Get Auth Information
MFEs are not independent security domains.
They are guests inside the shell.
Common patterns:
-
Shared Auth Service
-
Shared State Store
-
Global Event / Signal
Conceptually:
“Shell authenticates once, MFEs consume identity.”
MFEs trust the shell.
6️⃣ Role-Based Access (Very Important)
Authorization happens at two levels:
🔹 UI Level
-
Which MFEs are visible?
-
Which routes are accessible?
-
Which buttons are enabled?
🔹 API Level
-
Backend validates token
-
Backend checks role claims
Even if a user hacks the UI:
-
Backend still enforces rules
This is defense in depth.
7️⃣ Routing + Auth (How They Work Together)
The shell:
-
Owns the router
-
Guards routes
-
Decides which MFE loads
Example (conceptually):
-
Admin → can load Admin MFE
-
Manager → can load Wealth MFE
-
Retail User → Retail MFE only
MFEs:
-
Assume access is already validated
-
Don’t re-check login
-
Only check feature-level permissions
8️⃣ Why MFEs Should NOT Authenticate Themselves
If each MFE did auth:
-
Multiple logins
-
Token conflicts
-
Logout chaos
-
Security gaps
Interview phrase:
“MFEs are runtime-integrated, not security-isolated.”
That sounds senior — because it is.
9️⃣ Token Propagation to Backend APIs
Flow:
-
Shell authenticates
-
Shell stores token
-
HTTP interceptor attaches token
-
API validates token
-
API enforces role-based rules
MFEs:
-
Use the same HTTP client configuration
-
Do not know how the token was obtained
This keeps MFEs backend-agnostic.
🔟 Logout (Often Forgotten, Interview Gold)
Logout is:
-
Shell clears token
-
Shell resets shared state
-
Shell unloads MFEs
-
User is redirected to login
MFEs don’t handle logout logic.
1️⃣1️⃣ Why This Scales in Enterprises
This approach supports:
-
Independent teams
-
Separate deployments
-
Central security audits
-
Zero-trust backend APIs
Which is why:
-
Banks
-
FinTechs
-
Government apps
🔁 State Management in Micro Frontend Architecture (Angular 21)
1️⃣ Why State Is Tricky in MFEs
In a single SPA:
-
One global store
-
One runtime
-
One ownership model
In MFEs:
-
Multiple independently built apps
-
Loaded at runtime
-
Possibly from different deployments
So the key question becomes:
What state should be shared — and what should not?
2️⃣ The Core Principle (Memorize This)
Share as little state as possible.
Most MFE failures happen because teams:
-
Share too much
-
Couple lifecycles
-
Break isolation
3️⃣ Types of State in an MFE System
Think in layers:
🔹 Global / Cross-App State
Shared by all MFEs:
-
Auth info
-
User profile
-
Theme
-
Locale
-
Feature flags
Owned by: Shell
🔹 Domain State
Owned by a specific MFE:
-
Merchant data
-
Wealth portfolios
-
Capital market instruments
-
Consumer lending details
Owned by: Each MFE
🔹 Local UI State
Component-level:
-
Form inputs
-
Dropdown state
-
Modals
Never shared.
4️⃣ Who Owns Shared State?
Always:
The Shell owns shared state.
MFEs:
-
Read from it
-
React to it
-
Never mutate it directly (unless via contracts)
This prevents:
-
Hidden side effects
-
Debugging nightmares
-
Accidental coupling
5️⃣ How State Is Shared (Conceptually)
Common enterprise patterns:
✅ Shared Runtime Singleton
-
Auth service
-
User context service
✅ Global Store
-
Lightweight shared store
-
Not full Redux unless needed
✅ Signals / Observables
-
Push-based updates
-
MFEs subscribe, not poll
Key idea:
MFEs are consumers, not owners.
6️⃣ Why Angular Signals Matter (Angular 21 Context)
Signals:
-
Are synchronous
-
Are explicit
-
Have clear ownership
This makes them ideal for:
-
Auth state
-
User info
-
Feature toggles
They reduce:
-
Over-engineered global stores
-
Boilerplate
-
Async confusion
7️⃣ What NOT to Share Between MFEs
🚫 Do NOT share:
-
Component state
-
Form data
-
Business workflows
-
Navigation history
Each MFE must remain:
-
Replaceable
-
Deployable
-
Testable on its own
8️⃣ State + Routing Relationship
Flow:
-
User logs in
-
Shell updates auth state
-
Router reacts
-
Correct MFE loads
-
MFE reads shared context
Routing decisions depend on:
-
Shared state
-
Not hardcoded rules
9️⃣ Common Anti-Patterns (Interview Gold)
❌ MFEs calling each other directly
❌ Shared mutable objects
❌ Global event chaos
❌ One giant shared Redux store
Interview phrase:
“Those patterns break isolation and defeat the purpose of MFEs.”
🔟 Real-World Example (Conceptual)
-
User logs in as Manager
-
Shell updates role state
-
Navigation updates
-
Wealth MFE becomes available
-
Merchant MFE hidden
-
APIs enforce same role rules
No MFE talks to another MFE.
💥 Why Micro Frontends Fail in Practice
Micro frontends don’t usually fail because of technology.
They fail because of people, process, and misuse.
1️⃣ MFEs Are Used When They’re Not Needed (Most Common)
What happens:
-
Small team
-
Single UI
-
Tight coupling
-
Still choose MFEs “for scalability”
Result:
-
More repos
-
More builds
-
Slower delivery
Truth:
If one team owns the UI, MFEs add complexity with no payoff.
Interview line:
“MFEs are an organizational scalability tool, not a technical one.”
2️⃣ Teams Share Too Much State
What happens:
-
Shared global stores
-
Shared domain objects
-
MFEs mutate each other’s data
Result:
-
Hidden dependencies
-
Race conditions
-
Impossible debugging
Why it fails:
Isolation is broken, but complexity remains.
3️⃣ Auth Is Implemented Multiple Times
What happens:
-
Each MFE handles login
-
Tokens stored in multiple places
-
Inconsistent logout behavior
Result:
-
Security gaps
-
Token desync
-
Broken sessions
Correct model:
One auth boundary. Always the shell.
4️⃣ Routing Is Not Centralized
What happens:
-
Each MFE defines its own top-level routes
-
Deep links break
-
Navigation becomes unpredictable
Result:
-
UX inconsistency
-
SEO issues
-
Hard-to-test flows
Rule:
The shell owns global routing. MFEs own internal routing only.
5️⃣ Build & Tooling Explosion
What happens:
-
Different Angular versions
-
Different build pipelines
-
Different lint rules
Result:
-
Integration hell
-
CI/CD fragility
-
“Works on my machine”
Why it fails:
MFEs increase coordination cost — tooling must be standardized.
6️⃣ Performance Gets Worse, Not Better
What happens:
-
Too many remote bundles
-
Duplicate dependencies
-
Poor lazy-loading strategy
Result:
-
Slower startup
-
Increased memory usage
Reality:
MFEs trade runtime simplicity for organizational flexibility.
If performance isn’t measured, MFEs quietly hurt UX.
7️⃣ Teams Don’t Have Clear Ownership
What happens:
-
Everyone touches everything
-
No clear domain boundaries
-
Shared responsibility without accountability
Result:
-
Slow decisions
-
Constant breaking changes
Rule:
Each MFE must have a clear owner and domain contract.
8️⃣ Versioning & Contract Drift
What happens:
-
Shell expects one API
-
MFE ships another
-
No backward compatibility
Result:
-
Runtime failures
-
Emergency rollbacks
Fix:
Treat MFEs like external consumers — versioned, documented contracts.
9️⃣ Over-Engineering State Management
What happens:
-
Redux, NgRx, event buses everywhere
-
Complex sync logic
Result:
-
Hard to reason about
-
Slower onboarding
-
Fragile flows
Truth:
Most MFEs only need auth + user context shared.
🔟 MFEs Hide Organizational Problems (The Big One)
What happens:
-
Poor communication
-
Unclear requirements
-
Weak product ownership
MFEs amplify these problems instead of fixing them.
Brutal truth:
MFEs don’t fix bad teams — they expose them.
1️⃣1️⃣ When MFEs Actually Succeed
MFEs work when:
-
Multiple teams
-
Clear domain boundaries
-
Strong platform team
-
Centralized auth & routing
-
Minimal shared state
-
Strict contracts
Without these, MFEs fail fast.
🚀 Real Production Deployment Patterns for Micro Frontends
1️⃣ The Core Production Question
In production, the key question is not:
“How do we build MFEs?”
It is:
“How do we deploy and update MFEs safely without breaking users?”
Everything else follows from this.
2️⃣ The Three Real-World Deployment Models
🔹 Model 1: Independent Deployment (Most Common & Recommended)
How it works:
-
Each MFE has:
-
Its own repo
-
Its own CI/CD pipeline
-
Its own deployment target
-
-
Shell loads MFEs at runtime via URLs
Characteristics:
-
Teams deploy independently
-
No coordinated releases
-
Rollbacks are localized
Used by:
-
Banks
-
Large SaaS products
-
Government portals
Interview line:
“Each MFE is deployed independently and discovered by the shell at runtime.”
🔹 Model 2: Version-Pinned Deployment (Safer but Slower)
How it works:
-
Shell explicitly pins MFE versions
-
MFEs publish versioned artifacts
-
Shell upgrade is deliberate
Characteristics:
-
More control
-
Fewer surprises
-
Slower rollout
Used by:
-
Regulated environments
-
Legacy-heavy systems
🔹 Model 3: Unified Release (Anti-Pattern for MFEs)
How it works:
-
All MFEs released together
-
Single pipeline
-
Tight coupling
Why it fails:
-
No independence
-
No real MFE benefit
Interview phrase:
“At that point, you might as well use a monolith.”
3️⃣ Where MFEs Are Actually Hosted
Common production setups:
✅ CDN-based hosting
-
Static assets
-
Fast global delivery
-
Cache control per MFE
✅ Object storage
-
S3 / Azure Blob / GCS
-
Versioned artifacts
✅ Reverse proxy
-
Nginx / Envoy
-
Routing based on path or subdomain
Shell never cares where MFEs live — only their URLs.
4️⃣ Runtime Discovery (Critical Concept)
Shell does not hardcode builds.
Instead, it:
-
Fetches remote entry points
-
Loads bundles dynamically
-
Integrates at runtime
This allows:
-
Hot updates
-
Canary releases
-
Gradual rollouts
Interview phrase:
“MFEs are integrated at runtime, not build time.”
5️⃣ Environment Configuration Strategy
Production-safe approach:
-
Shell knows:
-
Which environment it’s in
-
-
MFEs know:
-
Their own backend URLs
-
Avoid:
-
Hardcoded environments
-
Cross-MFE env coupling
This enables:
-
Blue/green deployments
-
Region-based routing
6️⃣ Rollback Strategy (Very Important)
If an MFE breaks:
-
Roll back only that MFE
-
Shell remains untouched
-
Other MFEs keep working
This is one of the biggest advantages of MFEs.
Interview line:
“MFEs allow localized rollback instead of full application rollback.”
7️⃣ Feature Flags + MFEs (Power Combo)
Production teams often combine:
-
MFEs + feature flags
This allows:
-
Gradual exposure
-
Role-based rollout
-
A/B testing
Shell decides:
-
Who sees which MFE
-
Based on user context
8️⃣ Failure Isolation (Underrated Benefit)
In production:
-
One MFE may fail to load
-
Shell still renders
-
Other MFEs continue working
Good shells:
-
Show fallback UI
-
Log telemetry
-
Don’t crash the entire app
This is resilience by design.
9️⃣ Observability & Monitoring
Each MFE:
-
Logs independently
-
Emits metrics
-
Has its own dashboards
Shell:
-
Correlates user sessions
-
Tracks load failures
MFEs are treated like:
“Frontend services”
🔟 Security in Production
Key points:
-
Tokens never embedded in builds
-
Auth handled at runtime
-
Backend APIs remain zero-trust
Production shells:
-
Validate token presence
-
Enforce route access
-
Never assume MFE correctness
Quick difference (1 line each)
✅ Native Federation (what you have)
-
Uses:
remoteEntry.json+ federation manifest -
Loads: ESM-based remote modules
-
Designed for: modern Angular (17+) / Angular 21
✅ Module Federation (classic Webpack-based)
-
Uses:
remoteEntry.js -
Webpack runtime integration
-
Common in Angular 12–16 setups
Angular 21 Micro Frontend Deployment on Ubuntu with Nginx (Shell + Independent Remotes)
This document describes how to deploy an Angular 21 Micro Frontend (MFE) solution using Native Federation (@angular-architects/native-federation), Ubuntu Server, Nginx for static hosting, and Cloudflare SSL (Flexible mode). It follows a production-style micro-frontend deployment pattern: one Shell (Host) application and multiple Remote applications deployed independently.
1) Target Architecture
| Component | Type | URL |
|---|---|---|
| Shell | Host | https://angular21mfe.ihaveverygoodwebsite.com |
| Merchant Services | Remote | https://merchantservice.ihaveverygoodwebsite.com |
| Wealth | Remote | https://wealth.ihaveverygoodwebsite.com |
| Credit | Remote | https://credit.ihaveverygoodwebsite.com |
Why subdomains? Subdomains keep deployments clean and scalable: each Remote can be deployed independently, the Shell can dynamically load remotes from different locations, and the system avoids base-href/subpath complexities.
2) Build Output Structure (Angular 21)
This project generates production builds in the following format:
dist/<app-name>/browser/index.html
dist/<app-name>/browser/*.js
Examples:
dist/shell/browser/
dist/merchant/browser/
dist/credit/browser/
dist/wealth/browser/
3) Server Deployment Folder Layout (Ubuntu)
The following directories are created on the Ubuntu server:
/var/www/angular21mfe/shell
/var/www/angular21mfe/merchant
/var/www/angular21mfe/credit
/var/www/angular21mfe/wealth
4) Copy Build Output to the Server
For each app, copy the contents of the browser/ folder into the target directory.
Shell
sudo mkdir -p /var/www/angular21mfe/shell
sudo rsync -av --delete dist/shell/browser/ /var/www/angular21mfe/shell/
Merchant
sudo mkdir -p /var/www/angular21mfe/merchant
sudo rsync -av --delete dist/merchant/browser/ /var/www/angular21mfe/merchant/
Credit
sudo mkdir -p /var/www/angular21mfe/credit
sudo rsync -av --delete dist/credit/browser/ /var/www/angular21mfe/credit/
Wealth
sudo mkdir -p /var/www/angular21mfe/wealth
sudo rsync -av --delete dist/wealth/browser/ /var/www/angular21mfe/wealth/
Apply permissions:
sudo chown -R www-data:www-data /var/www/angular21mfe
sudo chmod -R 755 /var/www/angular21mfe
5) Cloudflare DNS Setup (Required)
Create DNS entries for each subdomain and enable proxy mode (orange cloud). Cloudflare SSL mode used in this setup: Flexible.
- angular21mfe.ihaveverygoodwebsite.com
- merchantservice.ihaveverygoodwebsite.com
- credit.ihaveverygoodwebsite.com
- wealth.ihaveverygoodwebsite.com
6) Nginx Configuration Files
Each app uses a dedicated Nginx configuration file (as per your naming convention):
- angular21mfe.ihaveverygoodwebsite.com.conf
- merchantservice.ihaveverygoodwebsite.com.conf
- wealth.ihaveverygoodwebsite.com.conf
- credit.ihaveverygoodwebsite.com.conf
Each config supports:
- Static hosting
- SPA routing fallback (try_files ... /index.html)
- Cross-subdomain federation loading using CORS headers (remotes only)
- Non-caching of remoteEntry.json (remotes only)
7) Shell Nginx Config (Host App)
File: /etc/nginx/sites-available/angular21mfe.ihaveverygoodwebsite.com.conf
Purpose: Serve the Shell application from /var/www/angular21mfe/shell with SPA routing fallback.
server {
listen 80;
listen [::]:80;
server_name angular21mfe.ihaveverygoodwebsite.com;
root /var/www/angular21mfe/shell;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
8) Remote Nginx Config Template (Merchant / Credit / Wealth)
- Static hosting for the remote build
- CORS headers so the Shell can fetch remoteEntry.json cross-domain
- remoteEntry.json should not be cached (important for federation updates)
8.1 Merchant Remote Config
File: /etc/nginx/sites-available/merchantservice.ihaveverygoodwebsite.com.conf
server {
listen 80;
listen [::]:80;
server_name merchantservice.ihaveverygoodwebsite.com;
root /var/www/angular21mfe/merchant;
index index.html;
add_header Access-Control-Allow-Origin "https://angular21mfe.ihaveverygoodwebsite.com" always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept, Authorization" always;
add_header Vary "Origin" always;
location = /remoteEntry.json {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
}
8.2 Wealth Remote Config
File: /etc/nginx/sites-available/wealth.ihaveverygoodwebsite.com.conf
server {
listen 80;
listen [::]:80;
server_name wealth.ihaveverygoodwebsite.com;
root /var/www/angular21mfe/wealth;
index index.html;
add_header Access-Control-Allow-Origin "https://angular21mfe.ihaveverygoodwebsite.com" always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept, Authorization" always;
add_header Vary "Origin" always;
location = /remoteEntry.json {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
}
8.3 Credit Remote Config
File: /etc/nginx/sites-available/credit.ihaveverygoodwebsite.com.conf
server {
listen 80;
listen [::]:80;
server_name credit.ihaveverygoodwebsite.com;
root /var/www/angular21mfe/credit;
index index.html;
add_header Access-Control-Allow-Origin "https://angular21mfe.ihaveverygoodwebsite.com" always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept, Authorization" always;
add_header Vary "Origin" always;
location = /remoteEntry.json {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
}
9) Enable Sites and Reload Nginx
Enable each site configuration and reload Nginx:
sudo ln -sf /etc/nginx/sites-available/angular21mfe.ihaveverygoodwebsite.com.conf /etc/nginx/sites-enabled/
sudo ln -sf /etc/nginx/sites-available/merchantservice.ihaveverygoodwebsite.com.conf /etc/nginx/sites-enabled/
sudo ln -sf /etc/nginx/sites-available/credit.ihaveverygoodwebsite.com.conf /etc/nginx/sites-enabled/
sudo ln -sf /etc/nginx/sites-available/wealth.ihaveverygoodwebsite.com.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
10) Update the Shell Federation Manifest (Production URLs)
This project uses Native Federation. The Shell reads remote URLs from: /var/www/angular21mfe/shell/federation.manifest.json
Update the manifest to point to the deployed remote subdomains:
{
"merchant": "https://merchantservice.ihaveverygoodwebsite.com/remoteEntry.json",
"credit": "https://credit.ihaveverygoodwebsite.com/remoteEntry.json",
"wealth": "https://wealth.ihaveverygoodwebsite.com/remoteEntry.json"
}
11) Validation Checklist
Confirm Shell loads
Open: https://angular21mfe.ihaveverygoodwebsite.com
Confirm remoteEntry is reachable
Open these in a browser (each should return JSON):
- https://merchantservice.ihaveverygoodwebsite.com/remoteEntry.json
- https://credit.ihaveverygoodwebsite.com/remoteEntry.json
- https://wealth.ihaveverygoodwebsite.com/remoteEntry.json
Confirm end-to-end navigation
From the Shell website, navigating to /merchant, /credit, and /wealth should dynamically load each remote module.
12) Key Benefits of This Deployment
- Independent deployments: Each remote can be deployed and upgraded independently.
- Central orchestration: The Shell provides a unified login, layout, and routing experience.
- Enterprise-friendly: Clean separation by domain and scalable CI/CD design.
- Cloudflare-compatible: HTTPS handled at edge, origin remains simple, and remotes load securely via HTTPS URLs.
Remotes:
• https://merchantservice.ihaveverygoodwebsite.com
• https://credit.ihaveverygoodwebsite.com
• https://wealth.ihaveverygoodwebsite.com