DEV Community

Hemant Singh
Hemant Singh

Posted on

Building Scalable Applications with Micro-Frontends: A Practical Guide

MFE

Deployment structure ( MFE Demo ):

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:

  1. Host Application (Shell): The main application that orchestrates everything
  2. Remote Applications: Independent micro-frontends loaded dynamically
  3. Shared Libraries: Common code, utilities, and services

Micro-Frontend Architecture

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
Enter fullscreen mode Exit fullscreen mode

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',
  },
};
Enter fullscreen mode Exit fullscreen mode

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
];
Enter fullscreen mode Exit fullscreen mode

πŸ”§ 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
Enter fullscreen mode Exit fullscreen mode

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',
  },
};
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

πŸ’» 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/',
  },
};
Enter fullscreen mode Exit fullscreen mode

Build for Production

# Build host
nx build mfeui --configuration=production

# Build remotes
nx build products --configuration=production
nx build cart --configuration=production
# ... etc
Enter fullscreen mode Exit fullscreen mode

Deployment Structure

https://hemantajax.github.io/mfedemos/
β”œβ”€β”€ index.html (Host)
β”œβ”€β”€ products/
β”‚   └── remoteEntry.js
β”œβ”€β”€ cart/
β”‚   └── remoteEntry.js
β”œβ”€β”€ profile/
β”‚   └── remoteEntry.js
└── ...
Enter fullscreen mode Exit fullscreen mode

πŸ”„ 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));
  }
}
Enter fullscreen mode Exit fullscreen mode

Using Shared Services

In Products Remote:

export class ProductListComponent {
  private stateService = inject(StateService);

  addToCart(product: Product): void {
    // Add product logic...
    this.stateService.addToCart();
  }
}
Enter fullscreen mode Exit fullscreen mode

In Cart Remote:

export class CartComponent {
  private stateService = inject(StateService);

  cartItems = this.stateService.cartItems;

  removeItem(item: CartItem): void {
    // Remove item logic...
    this.stateService.removeFromCart();
  }
}
Enter fullscreen mode Exit fullscreen mode

In Host Navigation:

export class NavbarComponent {
  private stateService = inject(StateService);

  cartCount = this.stateService.cartItems;
}
Enter fullscreen mode Exit fullscreen mode

Real-World Example

For a complete guide on implementing shared services with state management, event communication, and best practices, check out:

πŸ“š Shared Services Guide

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

  1. Start Simple: Begin with a host and 2-3 remotes
  2. Shared Libraries: Create shared code for common functionality
  3. Service Communication: Use singleton services for cross-micro-frontend communication
  4. Environment Configs: Separate configs for local and production
  5. 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:


πŸ’¬ 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)