Deployment structure ( MFE Demo ):
-
Remotes:
β’ products: https://hemantajax.github.io/mfedemos/products/β’ cart: https://hemantajax.github.io/mfedemos/cart/
β’ profile: https://hemantajax.github.io/mfedemos/profile/
β’ orders: https://hemantajax.github.io/mfedemos/orders/
β’ analytics: https://hemantajax.github.io/mfedemos/analytics/
β’ notifications: https://hemantajax.github.io/mfedemos/notifications/
β’ messages: https://hemantajax.github.io/mfedemos/messages/
β’ admin: https://hemantajax.github.io/mfedemos/admin/
Introduction
In today's fast-paced development landscape, building scalable and maintainable applications is crucial. Micro-frontends extend the microservices concept to frontend development, allowing teams to work independently while delivering a unified user experience.
In this article, I'll share a practical approach to building micro-frontend applications using Angular 20, Module Federation, and Nx Workspace.
π― What You'll Learn
- Creating a Host (Shell) application
- Building Remote micro-frontend applications
- Local development setup
- Production deployment strategy
- Sharing services across micro-frontends
ποΈ Architecture Overview
A micro-frontend architecture consists of:
- Host Application (Shell): The main application that orchestrates everything
- Remote Applications: Independent micro-frontends loaded dynamically
- Shared Libraries: Common code, utilities, and services
The diagram above shows how the Host (Shell) application dynamically loads multiple remote micro-frontends, all sharing common services and state management.
π¦ Step 1: Creating the Host Application
// one time only
npx nx g @nx/angular:setup-mf mfeui --mfType=host --port=4200
The host application is your main shell that loads remote micro-frontends dynamically.
Module Federation Config (Host)
// module-federation.config.ts
module.exports = {
name: 'mfeui',
remotes: {
products: 'http://localhost:4201',
cart: 'http://localhost:4202',
profile: 'http://localhost:4203',
orders: 'http://localhost:4204',
},
};
Routing Configuration
// app.routes.ts
export const appRoutes: Route[] = [
{
path: 'products',
loadChildren: () => import('products/Routes').then((m) => m.remoteRoutes),
},
{
path: 'cart',
loadChildren: () => import('cart/Routes').then((m) => m.remoteRoutes),
},
// ... more routes
];
π§ Step 2: Creating Remote Applications
npx nx g @nx/angular:remote apps/<remote-name> --host=mfeui --port=<PORT> --style=scss
// Ex.
npx nx g @nx/angular:remote apps/products --host=mfeui --port=4201 --style=scss
npx nx g @nx/angular:remote apps/cart --host=mfeui --port=4202 --style=scss
Each remote is an independent Angular application that can be developed, tested, and deployed separately.
Module Federation Config (Remote)
// module-federation.config.ts (products remote)
module.exports = {
name: 'products',
exposes: {
'./Routes': 'apps/products/src/app/remote-entry/entry.routes.ts',
},
};
Remote Entry Component
// remote-entry.component.ts
@Component({
selector: 'app-products-entry',
template: `
<div class="container mt-4">
<h2>Products Micro-Frontend</h2>
<router-outlet></router-outlet>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RemoteEntryComponent {}
π» Local Development
Running your micro-frontends locally is straightforward:
Start Individual Applications
# Terminal 1 - Host
nx serve mfeui --port 4200
# Terminal 2 - Products Remote
nx serve products --port 4201
# Terminal 3 - Cart Remote
nx serve cart --port 4202
Start All at Once
# Use the development script
npm start
# This runs all applications concurrently:
# - Host (mfeui): http://localhost:4200
# - Products: http://localhost:4201
# - Cart: http://localhost:4202
# - Profile: http://localhost:4203
# - Orders: http://localhost:4204
# - Analytics: http://localhost:4205
# - Notifications: http://localhost:4206
# - Messages: http://localhost:4207
# - Admin: http://localhost:4208
The host will automatically load remotes from their respective ports during development.
π Production Deployment
For production, the configuration changes to use absolute URLs:
Production Module Federation Config
// module-federation.config.prod.ts
module.exports = {
name: 'mfeui',
remotes: {
products: 'https://hemantajax.github.io/mfedemos/products/',
cart: 'https://hemantajax.github.io/mfedemos/cart/',
profile: 'https://hemantajax.github.io/mfedemos/profile/',
orders: 'https://hemantajax.github.io/mfedemos/orders/',
analytics: 'https://hemantajax.github.io/mfedemos/analytics/',
notifications: 'https://hemantajax.github.io/mfedemos/notifications/',
messages: 'https://hemantajax.github.io/mfedemos/messages/',
admin: 'https://hemantajax.github.io/mfedemos/admin/',
},
};
Build for Production
# Build host
nx build mfeui --configuration=production
# Build remotes
nx build products --configuration=production
nx build cart --configuration=production
# ... etc
Deployment Structure
https://hemantajax.github.io/mfedemos/
βββ index.html (Host)
βββ products/
β βββ remoteEntry.js
βββ cart/
β βββ remoteEntry.js
βββ profile/
β βββ remoteEntry.js
βββ ...
π Sharing Services Across Micro-Frontends
One of the most powerful features is sharing state and services across micro-frontends. This is where the magic happens!
Shared Service Architecture
// libs/shared/services/src/lib/state.service.ts
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class StateService {
// Shared state using Angular signals
private cartItemsSignal = signal<number>(0);
cartItems = this.cartItemsSignal.asReadonly();
addToCart(): void {
this.cartItemsSignal.update((count) => count + 1);
}
removeFromCart(): void {
this.cartItemsSignal.update((count) => Math.max(0, count - 1));
}
}
Using Shared Services
In Products Remote:
export class ProductListComponent {
private stateService = inject(StateService);
addToCart(product: Product): void {
// Add product logic...
this.stateService.addToCart();
}
}
In Cart Remote:
export class CartComponent {
private stateService = inject(StateService);
cartItems = this.stateService.cartItems;
removeItem(item: CartItem): void {
// Remove item logic...
this.stateService.removeFromCart();
}
}
In Host Navigation:
export class NavbarComponent {
private stateService = inject(StateService);
cartCount = this.stateService.cartItems;
}
Real-World Example
For a complete guide on implementing shared services with state management, event communication, and best practices, check out:
This guide covers:
- β State management across micro-frontends
- β Event-driven communication
- β Service singleton patterns
- β Best practices and gotchas
π Benefits We've Achieved
1. Independent Development
- Teams work on separate micro-frontends without conflicts
- Each team owns their deployment pipeline
2. Scalability
- Add new features as new micro-frontends
- Scale teams independently
3. Technology Flexibility
- Different versions of Angular (if needed)
- Different UI libraries per micro-frontend
- Gradual migration capabilities
4. Performance
- Lazy loading of micro-frontends
- Load only what users need
- Reduced initial bundle size
5. Maintainability
- Smaller codebases per micro-frontend
- Easier to understand and modify
- Better test coverage
π Key Takeaways
- Start Simple: Begin with a host and 2-3 remotes
- Shared Libraries: Create shared code for common functionality
- Service Communication: Use singleton services for cross-micro-frontend communication
- Environment Configs: Separate configs for local and production
- Documentation: Keep your architecture documented (crucial for team onboarding)
π Live Demo & Source Code
Want to see it in action?
π Live Demo: https://hemantajax.github.io/mfedemos/
π» Source Code: https://github.com/hemantajax/mfedemos
Explore the Documentation:
- π Complete Documentation Index
- π§ Shared Services Guide
- ποΈ Libraries Architecture
- π Port Guide
π¬ Final Thoughts
Micro-frontends are not just about splitting code β they're about enabling team autonomy, improving scalability, and creating a more maintainable architecture.
The learning curve is worth it, especially for large-scale applications with multiple teams.
Top comments (0)