Angular 115 🅰️ Micro-Frontends with Angular
A micro-frontend architecture splits a large Angular application into smaller, independently developed and deployed pieces that compose into a single user experience. Each micro-frontend has its own codebase, build pipeline, and deployment lifecycle, allowing separate teams to work autonomously without coordinating releases through a central repository. The application shell handles routing and loads micro-frontends on demand, stitching them together at runtime rather than compile time.
This approach is not a default recommendation from the Angular team. Angular was designed for compile-time optimization, type checking, and tree-shaking across a single codebase. Micro-frontends move integration from compile time to runtime, which undermines some of these benefits and introduces complexity that a monorepo with compile-time integration would avoid . The Angular team recommends evaluating whether a monorepo with shared libraries is sufficient before adopting micro-frontends, because the operational overhead of runtime integration is substantial .
When micro-frontends are justified — typically by organizational boundaries that prevent a monorepo, or by the need to integrate applications built with different frameworks — Angular supports several implementation approaches. This chapter covers the architectural decisions, the primary implementation strategies, and the practical concerns that determine whether the approach succeeds or fails.
Key point: Micro-frontends trade compile-time integration for runtime composition, enabling independent deployment at the cost of complexity. The Angular team recommends monorepos with shared libraries as the first alternative; micro-frontends are justified when organizational boundaries or framework diversity make a monorepo impractical.
Why micro-frontends exist
The organizational scaling problem. A monorepo with a single build works until the number of teams and the frequency of changes make coordination expensive. When twenty teams contribute to one repository, every merge affects the shared build, and a broken change in one team’s code can block every other team’s deployment. Micro-frontends give each team its own repository, build, and deployment pipeline, decoupling release schedules .
The incremental migration problem. Legacy AngularJS applications cannot be upgraded to modern Angular in a single step. Micro-frontends allow a shell to host both the old application and new Angular micro-frontends side by side, migrating one route or feature at a time . The shell routes to the legacy application for unmigrated paths and to new micro-frontends for migrated paths, enabling a gradual transition without a full rewrite.
The framework diversity problem. When an organization acquires a company or merges teams that use React and Angular, a single-framework monorepo cannot accommodate both. A micro-frontend shell can load Angular remotes and React remotes through a framework-agnostic integration layer, presenting them as one application to the user . This is the multi-framework use case, which is more complex than single-framework micro-frontends but unavoidable in some organizational contexts.
The independence problem. In a monorepo, a dependency upgrade must be coordinated across every project. If Angular releases a new version, the entire monorepo upgrades together. In a micro-frontend architecture, each remote can upgrade Angular independently, as long as the shared dependencies remain compatible at runtime . This independence is the primary technical justification for the architecture, but it also introduces version compatibility challenges.
The counterargument. The Angular team’s position is explicit: micro-frontends move integration from compile time to runtime, which means problems that would be caught by the compiler surface only when deployed applications interact . Runtime integration makes it impossible to verify the entire system before deployment. The recommendation is to exhaust monorepo options first, because the compile-time verification that Angular provides is lost in a micro-frontend architecture .
a. Architectural decisions and integration approaches
Micro-frontends can be integrated at several levels, and the choice determines the complexity of the implementation. Vertical split divides the application by route or feature area: one micro-frontend handles onboarding, another handles catalog, another handles checkout . Each micro-frontend is a complete vertical slice with its own views and logic. This is the recommended starting point because it minimizes coupling and is easier to reason about .
Horizontal split divides the application by technical layer: one micro-frontend handles the header, another handles the sidebar, another handles the main content area. This is more complex because the layers must coordinate to produce a coherent page, and it is generally discouraged for a first implementation .
Client-side composition loads micro-frontends in the browser, typically through route-based lazy loading. The shell’s router activates a micro-frontend when the user navigates to its route. Server-side composition assembles HTML on the server before sending it to the browser, which improves initial load performance but requires a server-side rendering infrastructure . Client-side composition is the common approach for Angular micro-frontends.
Application shell design. The shell should be technology-agnostic, stable, and free of domain-specific logic . It handles routing, authentication, and global configuration. If the shell is an Angular application, it should not contain business features that belong in micro-frontends. The heuristic from practitioners is that a well-designed shell should not change significantly after its first year .
b. Native Federation and Module Federation
Module Federation, introduced in webpack 5, is the original runtime integration mechanism for micro-frontends. It allows a host application to load remotes — separately built applications — at runtime. Dependencies like Angular and RxJS can be shared between host and remotes to avoid loading multiple copies . However, Module Federation is tied to webpack, and Angular’s new builder uses esbuild. This incompatibility led to Native Federation .
Native Federation is a bundler-agnostic implementation that uses ECMAScript modules and import maps instead of webpack’s proprietary runtime . It wraps Angular’s esbuild-based ApplicationBuilder and provides an API surface similar to Module Federation. The package @angular-architects/native-federation provides Angular schematics that configure a project as a host or remote .
Configuring a remote adds a federation.config.js file with an exposes object that lists what the remote makes available. By default, it exposes the root component, but routes can be exposed instead . The remote build produces a remoteEntry.json file that the host loads at runtime. The host’s main.ts calls initFederation() with a manifest that maps remote names to their URLs .
// federation.config.js for a remote
module.exports = withNativeFederation({
name: 'remote-app1',
exposes: {
'./Routes': './src/app/app.routes',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});
The shareAll configuration shares Angular and RxJS as singletons, ensuring the host and remotes use the same instance of these libraries . Version mismatches are detected at runtime and can be configured to warn or error.
c. Angular Elements as a composition mechanism
Angular Elements packages Angular components as custom elements — browser-native web components that any application can consume. This provides a different integration model: instead of the host loading a remote application through federation, the host loads a custom element that encapsulates the micro-frontend .
To create a custom element, the Angular module implements ngDoBootstrap() and calls createCustomElement() from @angular/elements. The component is then registered with customElements.define() . The host application includes the micro-frontend’s script and uses the custom element’s tag in its template.
The trade-off is that Angular Elements does not share dependencies the way federation does. The micro-frontend’s script bundles its own Angular runtime unless the host and micro-frontend are coordinated. This increases the downloaded payload. Federation is more efficient for Angular-to-Angular integration because it shares the framework runtime . Angular Elements is more appropriate when the host is not Angular, or when the integration boundary is a single component rather than a full application .
d. Single-SPA as an orchestration layer
Single-SPA is a framework-agnostic orchestrator that manages the lifecycle of multiple micro-frontends. It defines a standard interface — bootstrap, mount, unmount — that each micro-frontend implements, and handles routing to the appropriate one based on the URL .
IBM’s Sterling Store Engagement uses Single-SPA to orchestrate Angular micro-frontends, with each feature module as a separate Angular application. The applications share state through session storage or a shared context, and a vanilla JavaScript shell called store-root-app handles routing . The monorepo is managed with Lerna and Yarn workspaces to share source code across the application packages .
Single-SPA is more flexible than federation alone because it supports multiple frameworks and provides a clear lifecycle for micro-frontends. The cost is additional infrastructure: the orchestrator must be configured, each micro-frontend must implement the lifecycle interface, and the development workflow must accommodate multiple running applications .
e. Shared dependencies and state management
Dependency sharing is critical for performance. Without sharing, loading three Angular micro-frontends means downloading the Angular runtime three times. Federation’s shared configuration addresses this by declaring which packages are singletons — loaded once and shared across all micro-frontends .
State management is more difficult. A shared NgRx store cannot be shared across independently built micro-frontends because each has its own Angular DI context . The recommended approach is to move shared state into a library that is federated or linked, so all micro-frontends reference the same store instance. For NGXS, the state class must be in a shared library to avoid duplicate registration errors .
An alternative is communication through browser events. The CustomEvent API provides framework-agnostic messaging between micro-frontends, with each micro-frontend dispatching and listening for events on the window object . This decouples micro-frontends but loses type safety and makes the event contracts implicit rather than explicit.
f. Development and deployment concerns
Running multiple applications during development requires either multiple terminal windows or a tool that starts them in parallel. Nx supports this through its run-many command and a dev script that starts the shell and all remotes together . Native Federation provides automatic shell reloading when a remote finishes building, via server-sent events, eliminating manual refreshes .
Deployment of micro-frontends requires each remote to be deployed independently and the shell’s manifest to be updated with the remote URLs. Cross-origin resource sharing (CORS) must be configured on the remote’s hosting platform to allow the shell to load its assets . The shell needs a rewrite rule to serve index.html for all routes, or deep links will return 404 .
Observability is a common gap. Frontend developers often lack experience observing deployed systems, and micro-frontends increase the number of moving parts. Each micro-frontend should be monitored independently, and the integration points — routing, shared state, and event communication — should be instrumented to detect failures .
Complete Example Session
# ============================================
# PART 1: CREATE THE WORKSPACE
# ============================================
# A workspace without an application, for multiple projects.
ng new angular-mfe --create-application=false
cd angular-mfe
# ============================================
# PART 2: CREATE THE SHELL AND FIRST REMOTE
# ============================================
# The shell orchestrates; the remote provides a feature.
ng generate application shell --prefix app-shell
ng generate application products --prefix app-products
# ============================================
# PART 3: INSTALL NATIVE FEDERATION
# ============================================
# The schematic configures federation for a project.
npm i -D @angular-architects/native-federation
ng g @angular-architects/native-federation:init --project shell --port 4200 --type dynamic-host
ng g @angular-architects/native-federation:init --project products --port 4201 --type remote
// ============================================
// PART 4: CONFIGURE THE REMOTE'S EXPOSES
// ============================================
// federation.config.js for the products remote.
module.exports = withNativeFederation({
name: 'products',
exposes: {
'./Routes': './projects/products/src/app/app.routes',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});
// ============================================
// PART 5: CONFIGURE THE HOST'S MANIFEST
// ============================================
// federation.manifest.json in the shell's public folder.
{
"products": "http://localhost:4201/remoteEntry.json"
}
// ============================================
// PART 6: INITIALIZE FEDERATION IN THE SHELL
// ============================================
// main.ts in the shell loads the manifest and boots.
import { initFederation } from '@angular-architects/native-federation';
initFederation('federation.manifest.json')
.catch(err => console.error(err))
.then(_ => import('./bootstrap'))
.catch(err => console.error(err));
// ============================================
// PART 7: LAZY-LOAD THE REMOTE IN THE SHELL ROUTER
// ============================================
// The shell's routes load the remote's routes.
export const routes: Routes = [
{
path: 'products',
loadChildren: () => import('products/Routes').then(m => m.routes),
},
];
# ============================================
# PART 8: RUN THE APPLICATIONS
# ============================================
# Start shell and remote in separate terminals.
# Terminal 1:
ng serve shell
# Terminal 2:
ng serve products
# ============================================
# PART 9: DEPLOY AND CONFIGURE CORS
# ============================================
# Each app is deployed independently.
# The remote must allow the shell's origin.
# On the remote's hosting platform, add headers:
# Access-Control-Allow-Origin: *
# Access-Control-Allow-Methods: GET, OPTIONS
# Access-Control-Allow-Headers: *
# ============================================
# PART 10: SHELL REWRITE RULE
# ============================================
# The shell needs a fallback for client-side routing.
# On the shell's hosting platform, add a rewrite:
# From: /<*>
# To: /index.html
# Status: 200
These ten parts cover the core workflow: workspace setup, federation configuration, remote exposure, host manifest, runtime initialization, router integration, local development, and deployment configuration. The deployment steps are as important as the development steps because runtime integration fails without proper CORS and routing configuration.
Quick Reference
Implementation Approaches
| Approach | Integration | Framework Support | Complexity |
|---|---|---|---|
| Native Federation | Runtime, ESM | Angular, React, Vue | Medium |
| Module Federation | Runtime, webpack | Angular (webpack builder) | Medium |
| Angular Elements | Custom elements | Any host | Low-Medium |
| Single-SPA | Runtime orchestration | Any framework | High |
Federation Configuration
| File | Purpose |
|---|---|
federation.config.js | Exposes, shared dependencies, name |
federation.manifest.json | Maps remote names to URLs (host) |
remoteEntry.json | Generated remote metadata |
Shared Dependency Configuration
| Package | Configuration |
|---|---|
@angular/core | singleton: true, strictVersion: true |
@angular/common | Same |
@angular/router | Same |
rxjs | Same |
Deployment Checklist
| Item | Requirement |
|---|---|
| Shell rewrite | /<*> → /index.html, status 200 |
| Remote CORS | Access-Control-Allow-Origin: * |
| Remote URLs | Updated in manifest after deployment |
| Independent pipelines | One per micro-frontend |
Best Practices
✅ Do This:
// Share Angular as singleton
shared: { ...shareAll({ singleton: true }) } // Avoid duplicate framework loads
# Start with a vertical split
# One micro-frontend per route or feature area
# Configure CORS on remotes
# Allow the shell's origin
❌ Don’t Do This:
// Do not put domain logic in the shell
// The shell should route and authenticate only
# Do not skip CORS configuration
# Runtime loading will fail silently or with opaque errors
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Remote fails to load | CORS not configured | Add Access-Control-Allow-Origin headers |
| Duplicate Angular instances | Dependencies not shared | Configure singleton: true in shared |
| Deep links return 404 | No rewrite rule on shell host | Add /<*> → /index.html rewrite |
| State not shared | Separate DI contexts | Move shared state to a federated library |
| Version conflicts at runtime | Remotes use incompatible Angular versions | Use strictVersion: true and coordinate upgrades |
| Development refresh required | No live reload between shell and remote | Enable SSE reloading in initFederation |
Real-World Examples
1. Vertical Split by Feature
// Shell routes
const routes: Routes = [
{ path: 'onboarding', loadChildren: () => import('onboarding/Routes').then(m => m.routes) },
{ path: 'catalog', loadChildren: () => import('catalog/Routes').then(m => m.routes) },
];
2. Shared State Library
// libs/shared/state/src/lib/store.ts
export const store = new BehaviorSubject({ user: null });
3. Angular Element Integration
// Convert component to custom element
const el = createCustomElement(AppComponent, { injector });
customElements.define('angular-widget', el);
4. Single-SPA Lifecycle
// Each micro-frontend exports lifecycle functions
export function bootstrap(props) { /* ... */ }
export function mount(props) { /* ... */ }
export function unmount(props) { /* ... */ }
5. CustomEvent Communication
// Dispatch event from one micro-frontend
window.dispatchEvent(new CustomEvent('user-updated', { detail: { userId: 123 } }));
// Listen in another
window.addEventListener('user-updated', (e) => { /* ... */ });
6. SSE Live Reload
initFederation('federation.manifest.json', { sse: true })
7. Import Map Override for Local Development
// Override a deployed remote with a local one
localStorage.setItem('overrides-ui', true);
8. Nx Dev Script
{
"scripts": {
"dev": "nx run-many --target=serve --projects=shell,products"
}
}
9. Lerna Monorepo Management
# IBM's approach: Lerna + Yarn workspaces
lerna bootstrap
lerna run build
10. Error Page for Failed Remote Load
function loadMFE(url: string) {
return loadRemoteModule({
type: 'module',
remoteEntry: `${url}/remoteEntry.js`,
exposedModule: './Module'
}).catch(() => import('./error-page/error-page.module'));
}
Visual
Micro-Frontend Architecture
┌──────────────────────────────────────────────────────────────┐
│ SHELL + REMOTES WITH NATIVE FEDERATION │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ SHELL (Host) │ │
│ │ - Routing, authentication, global config │ │
│ │ - Loads remotes at runtime │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────┼───────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Remote A │ │ Remote B │ │ Remote C │ │
│ │ (Products) │ │ (Orders) │ │ (Profile) │ │
│ │ :4201 │ │ :4202 │ │ :4203 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Shared: @angular/core, @angular/common, rxjs (singletons) │
│ Each remote: independently built, deployed, and versioned │
└──────────────────────────────────────────────────────────────┘
Integration Decision Flow
┌──────────────────────────────────────────────────────────────┐
│ CHOOSING AN INTEGRATION APPROACH │
│ │
│ Do you have a monorepo with a single framework? │
│ │ │
│ ├── Yes ──▶ Use shared libraries, not micro-frontends │
│ │ (Angular team recommendation) │
│ │ │
│ └── No ──▶ Multiple repos or frameworks? │
│ │ │
│ ├── Same framework ──▶ Native Federation │
│ │ (Angular-to-Angular)│
│ │ │
│ └── Different frameworks ──▶ Single-SPA │
│ or Elements │
│ │
│ Start with vertical split, coarse-grained boundaries. │
│ Split further only when needed. │
└──────────────────────────────────────────────────────────────┘
Runtime Loading Flow
┌──────────────────────────────────────────────────────────────┐
│ HOW NATIVE FEDERATION LOADS A REMOTE │
│ │
│ 1. User navigates to /products │
│ │ │
│ 2. Shell router matches lazy route │
│ │ │
│ 3. Dynamic import: import('products/Routes') │
│ │ │
│ 4. Federation resolves 'products' from manifest │
│ │ │
│ 5. Fetches http://localhost:4201/remoteEntry.json │
│ │ │
│ 6. Loads remote's ES module bundle │
│ │ │
│ 7. Returns routes to shell router │
│ │ │
│ 8. Remote's component renders inside shell's router-outlet │
└──────────────────────────────────────────────────────────────┘
Dependency Sharing
┌──────────────────────────────────────────────────────────────┐
│ SHARED vs DUPLICATED DEPENDENCIES │
│ │
│ WITHOUT SHARING: │
│ Shell: @angular/core (1 copy) │
│ Remote A: @angular/core (1 copy) │
│ Remote B: @angular/core (1 copy) │
│ Total: 3 copies downloaded and executed │
│ │
│ WITH SHARING: │
│ Shell: @angular/core (1 copy) │
│ Remote A: uses shell's copy │
│ Remote B: uses shell's copy │
│ Total: 1 copy, shared runtime instance │
│ │
│ Configuration: shared: { ...shareAll({ singleton: true }) } │
│ │
│ Shared: @angular/core, @angular/common, @angular/router, │
│ rxjs. Not shared: application-specific libraries. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Shell | Host application that routes and loads micro-frontends |
| Remote | Independently built and deployed micro-frontend |
| Native Federation | Bundler-agnostic runtime integration using ESM |
| Module Federation | webpack 5 runtime integration |
| Angular Elements | Web components as micro-frontend boundary |
| Single-SPA | Framework-agnostic orchestration |
| Vertical split | By route or feature (recommended) |
| Horizontal split | By layout area (discouraged) |
| Shared deps | shareAll({ singleton: true }) |
| Deployment | Independent per remote; CORS + shell rewrite |
Key takeaways:
- Micro-frontends trade compile-time integration for runtime composition. The Angular team recommends monorepos with shared libraries as the first alternative, because runtime integration loses type checking and compile-time verification .
- Native Federation is the modern Angular approach. It uses ECMAScript modules and import maps, works with Angular’s esbuild builder, and provides an API similar to Module Federation .
- Vertical splits are the recommended starting point. Dividing by route or feature area minimizes coupling and is easier to reason about than horizontal splits .
- Dependency sharing is critical for performance. Angular and RxJS should be configured as singletons in the
sharedconfiguration to avoid loading multiple framework instances . - The shell should be technology-agnostic and stable. Domain logic belongs in micro-frontends, not in the shell .
- Deployment requires CORS and routing configuration. Remotes must allow cross-origin loading, and the shell needs a rewrite rule for client-side routes .
- State sharing requires a shared library. Separate DI contexts prevent direct store sharing; a federated library provides a common instance .
- Single-SPA and Angular Elements serve different needs. Single-SPA supports multi-framework orchestration; Elements provides a simpler web-component boundary .
Remember: Micro-frontends are an organizational solution to an organizational problem. The technical architecture exists to support team independence and incremental migration, not because runtime integration is inherently superior to compile-time integration. The Angular team’s recommendation to exhaust monorepo options first is grounded in the observation that Angular’s compiler and CLI are optimized for a single build, and runtime integration undermines that optimization . When micro-frontends are justified — by organizational boundaries, framework diversity, or legacy migration — Native Federation provides the most idiomatic Angular implementation, using ES modules and import maps to compose independently built applications. The shell routes, the remotes render, and the shared dependencies keep the user experience coherent. But the complexity is real: CORS configuration, deployment coordination, state sharing, and observability across multiple applications require operational maturity that a monorepo does not demand.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!