Skip to main content
Micro-frontends

Micro-frontends Overview

Estimated Time: 3 hours | Difficulty: Advanced | Prerequisites: Routing, Lazy Loading, Build Process
Micro-frontends extend the microservices concept to the frontend, allowing teams to build and deploy parts of a web application independently.
┌─────────────────────────────────────────────────────────────────────────┐
│                    Micro-frontend Architecture                           │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │                         Host (Shell)                             │   │
│   │   ┌─────────────────────────────────────────────────────────┐   │   │
│   │   │                    Navigation/Layout                     │   │   │
│   │   └─────────────────────────────────────────────────────────┘   │   │
│   │                                                                  │   │
│   │   ┌───────────────┐ ┌───────────────┐ ┌───────────────────┐    │   │
│   │   │    Remote 1   │ │    Remote 2   │ │     Remote 3      │    │   │
│   │   │   Dashboard   │ │    Products   │ │      Admin        │    │   │
│   │   │   (Team A)    │ │   (Team B)    │ │     (Team C)      │    │   │
│   │   └───────────────┘ └───────────────┘ └───────────────────┘    │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│                                                                          │
│   Benefits:                                                              │
│   • Independent deployment            • Technology agnostic             │
│   • Team autonomy                     • Smaller, focused codebases      │
│   • Incremental upgrades              • Parallel development            │
│                                                                          │
│   Challenges:                                                            │
│   • Shared state management           • Consistent styling              │
│   • Performance overhead              • Complex debugging               │
│   • Version compatibility             • Integration testing             │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Module Federation Setup

Using @angular-architects/module-federation

# Create workspace with multiple apps
npx create-nx-workspace@latest my-mfe --preset=empty
cd my-mfe

# Or with Angular CLI
ng new my-mfe-workspace --create-application=false
cd my-mfe-workspace

# Add host (shell) application
ng generate application shell --routing --style=scss

# Add remote applications
ng generate application dashboard --routing --style=scss
ng generate application products --routing --style=scss
ng generate application admin --routing --style=scss

# Add module federation to each app
ng add @angular-architects/module-federation --project shell --type host --port 4200
ng add @angular-architects/module-federation --project dashboard --type remote --port 4201
ng add @angular-architects/module-federation --project products --type remote --port 4202
ng add @angular-architects/module-federation --project admin --type remote --port 4203

Host (Shell) Configuration

// projects/shell/webpack.config.js
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({
  remotes: {
    dashboard: 'http://localhost:4201/remoteEntry.js',
    products: 'http://localhost:4202/remoteEntry.js',
    admin: 'http://localhost:4203/remoteEntry.js',
  },

  shared: {
    ...shareAll({
      singleton: true,
      strictVersion: true,
      requiredVersion: 'auto'
    }),
  },
});

// projects/shell/src/app/app.routes.ts
export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
  },
  {
    path: 'dashboard',
    loadChildren: () => import('dashboard/routes').then(m => m.DASHBOARD_ROUTES)
  },
  {
    path: 'products',
    loadChildren: () => import('products/routes').then(m => m.PRODUCTS_ROUTES)
  },
  {
    path: 'admin',
    loadChildren: () => import('admin/routes').then(m => m.ADMIN_ROUTES),
    canMatch: [adminGuard]
  }
];

// projects/shell/src/decl.d.ts
declare module 'dashboard/routes';
declare module 'products/routes';
declare module 'admin/routes';

Remote Configuration

// projects/dashboard/webpack.config.js
const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({
  name: 'dashboard',

  exposes: {
    './routes': './projects/dashboard/src/app/dashboard.routes.ts',
    './DashboardComponent': './projects/dashboard/src/app/dashboard/dashboard.component.ts',
  },

  shared: {
    ...shareAll({
      singleton: true,
      strictVersion: true,
      requiredVersion: 'auto'
    }),
  },
});

// projects/dashboard/src/app/dashboard.routes.ts
import { Routes } from '@angular/router';

export const DASHBOARD_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () => import('./dashboard/dashboard.component')
      .then(m => m.DashboardComponent)
  },
  {
    path: 'analytics',
    loadComponent: () => import('./analytics/analytics.component')
      .then(m => m.AnalyticsComponent)
  },
  {
    path: 'reports',
    loadComponent: () => import('./reports/reports.component')
      .then(m => m.ReportsComponent)
  }
];

Dynamic Remote Loading

// shell/src/app/services/mfe-registry.service.ts
import { loadRemoteModule } from '@angular-architects/module-federation';

export interface MfeConfig {
  name: string;
  remoteEntry: string;
  exposedModule: string;
  displayName: string;
  route: string;
  icon?: string;
}

@Injectable({ providedIn: 'root' })
export class MfeRegistryService {
  private http = inject(HttpClient);
  private router = inject(Router);
  
  private mfes = signal<MfeConfig[]>([]);
  readonly availableMfes = this.mfes.asReadonly();
  
  async loadRegistry(): Promise<void> {
    // Load MFE configuration from backend
    const config = await firstValueFrom(
      this.http.get<{ mfes: MfeConfig[] }>('/api/mfe-config')
    );
    
    this.mfes.set(config.mfes);
    this.registerRoutes(config.mfes);
  }
  
  private registerRoutes(mfes: MfeConfig[]): void {
    const routes: Routes = mfes.map(mfe => ({
      path: mfe.route,
      loadChildren: () => loadRemoteModule({
        type: 'module',
        remoteEntry: mfe.remoteEntry,
        exposedModule: mfe.exposedModule
      }).then(m => m.routes)
    }));
    
    // Add routes dynamically
    this.router.resetConfig([
      ...this.router.config.filter(r => r.path !== '**'),
      ...routes,
      { path: '**', redirectTo: '' }
    ]);
  }
}

// shell/src/app/app.component.ts
@Component({
  template: `
    <app-header />
    
    <nav class="sidebar">
      @for (mfe of registry.availableMfes(); track mfe.name) {
        <a [routerLink]="['/', mfe.route]" routerLinkActive="active">
          @if (mfe.icon) {
            <mat-icon>{{ mfe.icon }}</mat-icon>
          }
          {{ mfe.displayName }}
        </a>
      }
    </nav>
    
    <main>
      <router-outlet />
    </main>
  `
})
export class AppComponent implements OnInit {
  registry = inject(MfeRegistryService);
  
  async ngOnInit() {
    await this.registry.loadRegistry();
  }
}

Shared State & Communication

Shared State Service

// shared-lib/src/lib/shared-state.service.ts
import { Injectable, signal, computed } from '@angular/core';

export interface User {
  id: string;
  name: string;
  email: string;
  roles: string[];
}

export interface GlobalState {
  user: User | null;
  theme: 'light' | 'dark';
  notifications: Notification[];
  cart: CartItem[];
}

@Injectable({ providedIn: 'root' })
export class SharedStateService {
  private state = signal<GlobalState>({
    user: null,
    theme: 'light',
    notifications: [],
    cart: []
  });
  
  // Selectors
  readonly user = computed(() => this.state().user);
  readonly isAuthenticated = computed(() => this.state().user !== null);
  readonly theme = computed(() => this.state().theme);
  readonly notifications = computed(() => this.state().notifications);
  readonly cart = computed(() => this.state().cart);
  readonly cartCount = computed(() => this.state().cart.length);
  readonly cartTotal = computed(() => 
    this.state().cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
  );
  
  // Actions
  setUser(user: User | null) {
    this.state.update(s => ({ ...s, user }));
  }
  
  setTheme(theme: 'light' | 'dark') {
    this.state.update(s => ({ ...s, theme }));
    document.documentElement.classList.toggle('dark', theme === 'dark');
  }
  
  addNotification(notification: Notification) {
    this.state.update(s => ({
      ...s,
      notifications: [...s.notifications, notification]
    }));
  }
  
  removeNotification(id: string) {
    this.state.update(s => ({
      ...s,
      notifications: s.notifications.filter(n => n.id !== id)
    }));
  }
  
  addToCart(item: CartItem) {
    this.state.update(s => {
      const existing = s.cart.find(i => i.productId === item.productId);
      if (existing) {
        return {
          ...s,
          cart: s.cart.map(i => 
            i.productId === item.productId 
              ? { ...i, quantity: i.quantity + item.quantity }
              : i
          )
        };
      }
      return { ...s, cart: [...s.cart, item] };
    });
  }
  
  removeFromCart(productId: string) {
    this.state.update(s => ({
      ...s,
      cart: s.cart.filter(i => i.productId !== productId)
    }));
  }
  
  clearCart() {
    this.state.update(s => ({ ...s, cart: [] }));
  }
}

Event Bus for Cross-MFE Communication

// shared-lib/src/lib/event-bus.service.ts
import { Injectable } from '@angular/core';
import { Subject, Observable, filter, map } from 'rxjs';

export interface MfeEvent<T = any> {
  type: string;
  source: string;
  payload: T;
  timestamp: Date;
}

@Injectable({ providedIn: 'root' })
export class EventBusService {
  private events$ = new Subject<MfeEvent>();
  
  emit<T>(type: string, payload: T, source: string): void {
    this.events$.next({
      type,
      source,
      payload,
      timestamp: new Date()
    });
  }
  
  on<T>(type: string): Observable<MfeEvent<T>> {
    return this.events$.pipe(
      filter(event => event.type === type)
    );
  }
  
  onMultiple<T>(types: string[]): Observable<MfeEvent<T>> {
    return this.events$.pipe(
      filter(event => types.includes(event.type))
    );
  }
  
  // Predefined events
  emitNavigation(path: string, source: string) {
    this.emit('navigation', { path }, source);
  }
  
  emitUserAction(action: string, data: any, source: string) {
    this.emit('userAction', { action, data }, source);
  }
  
  emitError(error: Error, source: string) {
    this.emit('error', { message: error.message, stack: error.stack }, source);
  }
}

// Usage in remote MFE
@Component({...})
export class ProductDetailComponent {
  private eventBus = inject(EventBusService);
  private sharedState = inject(SharedStateService);
  
  addToCart(product: Product) {
    // Update shared state
    this.sharedState.addToCart({
      productId: product.id,
      name: product.name,
      price: product.price,
      quantity: 1
    });
    
    // Emit event for other MFEs
    this.eventBus.emit('cart:itemAdded', {
      productId: product.id,
      productName: product.name
    }, 'products');
  }
}

// Listening in shell
@Component({...})
export class HeaderComponent {
  private eventBus = inject(EventBusService);
  private snackBar = inject(MatSnackBar);
  
  constructor() {
    this.eventBus.on<{ productName: string }>('cart:itemAdded').subscribe(event => {
      this.snackBar.open(`${event.payload.productName} added to cart`, 'View Cart', {
        duration: 3000
      });
    });
  }
}

Shared UI Library

// shared-ui/src/lib/components/button/button.component.ts
@Component({
  selector: 'shared-button',
  standalone: true,
  template: `
    <button 
      [class]="buttonClass()"
      [disabled]="disabled()"
      [type]="type()"
    >
      @if (loading()) {
        <shared-spinner size="small" />
      }
      <ng-content />
    </button>
  `
})
export class SharedButtonComponent {
  variant = input<'primary' | 'secondary' | 'danger'>('primary');
  size = input<'small' | 'medium' | 'large'>('medium');
  disabled = input(false);
  loading = input(false);
  type = input<'button' | 'submit'>('button');
  
  buttonClass = computed(() => {
    return `btn btn-${this.variant()} btn-${this.size()}`;
  });
}

// shared-ui/src/lib/components/card/card.component.ts
@Component({
  selector: 'shared-card',
  standalone: true,
  template: `
    <div class="card" [class.card-elevated]="elevated()">
      @if (title()) {
        <div class="card-header">
          <h3>{{ title() }}</h3>
          <ng-content select="[card-actions]" />
        </div>
      }
      <div class="card-body">
        <ng-content />
      </div>
      @if (hasFooter) {
        <div class="card-footer">
          <ng-content select="[card-footer]" />
        </div>
      }
    </div>
  `
})
export class SharedCardComponent {
  title = input<string>();
  elevated = input(false);
  
  @ContentChild('card-footer') hasFooter = false;
}

// Export all shared components
// shared-ui/src/index.ts
export * from './lib/components/button/button.component';
export * from './lib/components/card/card.component';
export * from './lib/components/spinner/spinner.component';
export * from './lib/components/modal/modal.component';
export * from './lib/components/table/table.component';
export * from './lib/components/form-field/form-field.component';

Deployment Strategy

# docker-compose.mfe.yml
version: '3.8'

services:
  shell:
    build:
      context: ./projects/shell
      dockerfile: Dockerfile
    ports:
      - "4200:80"
    environment:
      - DASHBOARD_URL=http://dashboard:80
      - PRODUCTS_URL=http://products:80
      - ADMIN_URL=http://admin:80
    depends_on:
      - dashboard
      - products
      - admin

  dashboard:
    build:
      context: ./projects/dashboard
      dockerfile: Dockerfile
    ports:
      - "4201:80"

  products:
    build:
      context: ./projects/products
      dockerfile: Dockerfile
    ports:
      - "4202:80"

  admin:
    build:
      context: ./projects/admin
      dockerfile: Dockerfile
    ports:
      - "4203:80"

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - shell
      - dashboard
      - products
      - admin
# nginx.conf for micro-frontends
upstream shell {
    server shell:80;
}

upstream dashboard {
    server dashboard:80;
}

upstream products {
    server products:80;
}

upstream admin {
    server admin:80;
}

server {
    listen 80;
    
    location / {
        proxy_pass http://shell;
    }
    
    location /dashboard/ {
        proxy_pass http://dashboard/;
    }
    
    location /products/ {
        proxy_pass http://products/;
    }
    
    location /admin/ {
        proxy_pass http://admin/;
    }
    
    # Remote entries
    location ~ ^/(.+)/remoteEntry.js$ {
        set $mfe $1;
        proxy_pass http://$mfe/remoteEntry.js;
    }
}

Best Practices

Share Wisely

Only share what’s necessary - over-sharing leads to coupling

Version Compatibility

Test shared dependency versions across all MFEs

Consistent Styling

Use a shared design system with CSS custom properties

Error Boundaries

Isolate MFE failures to prevent cascade effects
// Error boundary wrapper
@Component({
  selector: 'mfe-error-boundary',
  template: `
    @if (hasError()) {
      <div class="mfe-error">
        <mat-icon>error_outline</mat-icon>
        <h3>Something went wrong</h3>
        <p>The {{ mfeName() }} module failed to load.</p>
        <button mat-button (click)="retry()">Retry</button>
      </div>
    } @else {
      <ng-content />
    }
  `
})
export class MfeErrorBoundaryComponent implements OnInit, ErrorHandler {
  mfeName = input.required<string>();
  hasError = signal(false);
  
  handleError(error: Error): void {
    console.error(`MFE Error [${this.mfeName()}]:`, error);
    this.hasError.set(true);
  }
  
  retry() {
    this.hasError.set(false);
  }
}

Next: Nx Monorepo

Scale your Angular projects with Nx monorepo tooling