Micro Frontends ๐๏ธ
Micro frontends extend microservices principles to frontend development, allowing teams to build, deploy, and scale features independently. Build modular, maintainable applications at scale.
๐ฏ What are Micro Frontends?
Section titled โ๐ฏ What are Micro Frontends?โMicro frontends are independently deployable frontend applications that work together as a single cohesive product.
Benefits:
- ๐ Independent Deployment - Deploy features separately
- ๐ฅ Team Autonomy - Teams own complete features
- โก Faster Development - Parallel development
- ๐ฏ Technology Flexibility - Mix frameworks if needed
- ๐ฆ Smaller Bundles - Load only whatโs needed
Approaches:
- Module Federation (requires Webpack โ see note below)
- Native Federation (works with esbuild)
- Web Components
- iFrames
- Server-side composition
Note: Angular uses esbuild as the default builder since Angular 17. Module Federation requires a Webpack-based build. Use
@angular-architects/native-federationfor an esbuild-compatible alternative, or configure your project to use the Webpack builder.
๐ Module Federation Setup
Section titled โ๐ Module Federation SetupโHost Configuration
Section titled โHost Configurationโconst ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'host', remotes: { dashboard: 'dashboard@http://localhost:4201/remoteEntry.js', profile: 'profile@http://localhost:4202/remoteEntry.js' }, shared: { '@angular/core': { singleton: true, strictVersion: true }, '@angular/common': { singleton: true, strictVersion: true } } }) ]};Remote Configuration
Section titled โRemote Configurationโ// webpack.config.js (Remote)module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'dashboard', filename: 'remoteEntry.js', exposes: { './Module': './src/app/dashboard/dashboard.routes.ts' }, shared: { '@angular/core': { singleton: true }, '@angular/common': { singleton: true } } }) ]};๐จ Real-World Examples
Section titled โ๐จ Real-World ExamplesโNote: Micro frontends require careful planning and team coordination.
1. Complete Shell Application
Section titled โ1. Complete Shell ApplicationโLetโs create a comprehensive shell application that manages multiple micro frontends.
import { Component, signal } from '@angular/core';import { RouterOutlet, RouterLink } from '@angular/router';
@Component({ selector: 'app-root', imports: [RouterOutlet, RouterLink], template: ` <div class="app-container"> <header class="app-header"> <h1>Enterprise Portal</h1> <nav> <a routerLink="/" routerLinkActive="active">Home</a> <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a> <a routerLink="/profile" routerLinkActive="active">Profile</a> <a routerLink="/analytics" routerLinkActive="active">Analytics</a> </nav> </header>
<main class="app-content"> <router-outlet></router-outlet> </main>
<footer class="app-footer"> <p>ยฉ 2024 Enterprise Portal</p> </footer> </div> `, styles: [` .app-container { display: flex; flex-direction: column; min-height: 100vh; } .app-header { background: #1976d2; color: white; padding: 1rem; } .app-header nav { display: flex; gap: 1rem; margin-top: 1rem; } .app-header a { color: white; text-decoration: none; padding: 0.5rem 1rem; border-radius: 4px; } .app-header a.active { background: rgba(255,255,255,0.2); } .app-content { flex: 1; padding: 2rem; } `]})export class AppComponent {}
// shell/app.routes.tsimport { Routes } from '@angular/router';
export const routes: Routes = [ { path: '', loadComponent: () => import('./home/home.component') .then(m => m.HomeComponent) }, { path: 'dashboard', loadChildren: () => import('dashboard/Module') .then(m => m.DASHBOARD_ROUTES) .catch(() => import('./fallback/fallback.routes') .then(m => m.FALLBACK_ROUTES)) }, { path: 'profile', loadChildren: () => import('profile/Module') .then(m => m.PROFILE_ROUTES) .catch(() => import('./fallback/fallback.routes') .then(m => m.FALLBACK_ROUTES)) }, { path: 'analytics', loadChildren: () => import('analytics/Module') .then(m => m.ANALYTICS_ROUTES) .catch(() => import('./fallback/fallback.routes') .then(m => m.FALLBACK_ROUTES)) }];2. Remote Dashboard Micro Frontend
Section titled โ2. Remote Dashboard Micro FrontendโLetโs create a complete remote micro frontend for dashboard.
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = { output: { uniqueName: 'dashboard', publicPath: 'auto' }, optimization: { runtimeChunk: false }, plugins: [ new ModuleFederationPlugin({ name: 'dashboard', filename: 'remoteEntry.js', exposes: { './Module': './src/app/dashboard/dashboard.routes.ts' }, shared: { '@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' }, '@angular/common': { singleton: true, strictVersion: true, requiredVersion: 'auto' }, '@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto' } } }) ]};
// dashboard/dashboard.routes.tsimport { Routes } from '@angular/router';import { DashboardComponent } from './dashboard.component';
export const DASHBOARD_ROUTES: Routes = [ { path: '', component: DashboardComponent }];
// dashboard/dashboard.component.tsimport { Component, signal, computed } from '@angular/core';
@Component({ selector: 'app-dashboard', template: ` <div class="dashboard"> <h2>Dashboard</h2>
<div class="stats-grid"> <div class="stat-card"> <h3>Total Users</h3> <p class="stat-value">{{ totalUsers() }}</p> </div>
<div class="stat-card"> <h3>Revenue</h3> <p class="stat-value">{{ revenue() }}</p> </div>
<div class="stat-card"> <h3>Active Sessions</h3> <p class="stat-value">{{ activeSessions() }}</p> </div> </div> </div> `, styles: [` .dashboard { padding: 2rem; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-top: 2rem; } .stat-card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .stat-value { font-size: 2rem; font-weight: bold; color: #1976d2; } `]})export class DashboardComponent { totalUsers = signal(1234); revenue = signal('$45,678'); activeSessions = signal(89);}3. Communication Between Micro Frontends
Section titled โ3. Communication Between Micro FrontendsโLetโs implement cross-micro-frontend communication.
import { Injectable } from '@angular/core';import { Subject, Observable } from 'rxjs';import { filter, map } from 'rxjs/operators';
export interface MicroFrontendEvent { type: string; source: string; payload: any;}
@Injectable({ providedIn: 'root' })export class EventBusService { private eventSubject = new Subject<MicroFrontendEvent>();
emit(type: string, source: string, payload: any) { this.eventSubject.next({ type, source, payload }); }
on(eventType: string): Observable<any> { return this.eventSubject.pipe( filter(event => event.type === eventType), map(event => event.payload) ); }
onFromSource(eventType: string, source: string): Observable<any> { return this.eventSubject.pipe( filter(event => event.type === eventType && event.source === source), map(event => event.payload) ); }}
// Usage in Dashboard@Component({ selector: 'app-dashboard',})export class DashboardComponent { private eventBus = inject(EventBusService);
notifyUserAction() { this.eventBus.emit('user-action', 'dashboard', { action: 'button-clicked', timestamp: Date.now() }); }
constructor() { // Listen to events from other micro frontends this.eventBus.on('profile-updated').subscribe(data => { console.log('Profile updated:', data); this.refreshDashboard(); }); }}4. Shared State Management
Section titled โ4. Shared State ManagementโLetโs create a shared state service for micro frontends.
import { Injectable, signal, computed } from '@angular/core';
export interface User { id: string; name: string; email: string; role: string;}
@Injectable({ providedIn: 'root' })export class SharedStateService { // Shared user state private currentUser = signal<User | null>(null);
// Shared app settings private appSettings = signal({ theme: 'light', language: 'en', notifications: true });
// Public readonly signals user = this.currentUser.asReadonly(); settings = this.appSettings.asReadonly();
// Computed values isAuthenticated = computed(() => this.currentUser() !== null); userRole = computed(() => this.currentUser()?.role ?? 'guest');
setUser(user: User) { this.currentUser.set(user); }
updateSettings(settings: Partial<typeof this.appSettings>) { this.appSettings.update(current => ({ ...current, ...settings })); }
logout() { this.currentUser.set(null); }}
// Usage in any micro frontend@Component({ selector: 'app-profile',})export class ProfileComponent { private sharedState = inject(SharedStateService);
user = this.sharedState.user; isAuthenticated = this.sharedState.isAuthenticated;
updateProfile(name: string) { const currentUser = this.user(); if (currentUser) { this.sharedState.setUser({ ...currentUser, name }); } }}5. Dynamic Remote Loading
Section titled โ5. Dynamic Remote LoadingโLetโs implement dynamic loading of micro frontends at runtime.
import { Injectable } from '@angular/core';
export interface RemoteConfig { name: string; url: string; exposedModule: string;}
@Injectable({ providedIn: 'root' })export class DynamicFederationService { private loadedRemotes = new Map<string, any>();
async loadRemoteModule(config: RemoteConfig) { // Check if already loaded if (this.loadedRemotes.has(config.name)) { return this.loadedRemotes.get(config.name); }
// Load remote container await this.loadRemoteContainer(config.url);
// Import the exposed module const container = (window as any)[config.name]; await container.init(__webpack_share_scopes__.default);
const factory = await container.get(config.exposedModule); const module = factory();
this.loadedRemotes.set(config.name, module); return module; }
private loadRemoteContainer(url: string): Promise<void> { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = url; script.type = 'text/javascript'; script.async = true;
script.onload = () => resolve(); script.onerror = () => reject(new Error(`Failed to load ${url}`));
document.head.appendChild(script); }); }}
// Usage in routesexport const routes: Routes = [ { path: 'dynamic/:moduleName', loadChildren: async () => { const federationService = inject(DynamicFederationService); const moduleName = inject(ActivatedRoute).snapshot.params['moduleName'];
const config = { name: moduleName, url: `http://localhost:${getPortForModule(moduleName)}/remoteEntry.js`, exposedModule: './Module' };
const module = await federationService.loadRemoteModule(config); return module.ROUTES; } }];โ Best Practices
Section titled โโ Best Practicesโ1. Shared Dependencies
Section titled โ1. Shared Dependenciesโ// โ
Share common librariesshared: { '@angular/core': { singleton: true }, '@angular/router': { singleton: true }}2. Version Management
Section titled โ2. Version Managementโ// โ
Strict version controlshared: { '@angular/core': { singleton: true, strictVersion: true, requiredVersion: '^18.0.0' }}3. Error Boundaries
Section titled โ3. Error Boundariesโ// โ
Handle remote loading errorsloadChildren: () => import('remote/Module') .catch(() => import('./fallback/fallback.routes'))๐ฏ Architecture Patterns
Section titled โ๐ฏ Architecture Patternsโ- Shell Pattern - Central shell loads remotes
- Composite Pattern - Multiple shells
- Federated Modules - Shared module federation
- Micro Apps - Fully independent apps
๐ Learning Checklist
Section titled โ๐ Learning Checklistโ- Understand micro frontend concepts
- Set up Module Federation
- Configure shared dependencies
- Handle version conflicts
- Implement error boundaries
- Deploy independently
- Monitor performance
๐ Next Steps
Section titled โ๐ Next Stepsโ- Bundle Optimization - Reduce bundle sizes
- Performance Optimization - Optimize applications
- Angular Elements - Create web components
Pro Tip: Start small with micro frontends! Begin with one remote module and gradually expand. Ensure proper communication patterns and shared state management! ๐๏ธ