Skip to main content

Command Palette

Search for a command to run...

Micro-Frontends Explained: How to Implement Module Federation

Updated
6 min read
T
I am a passionate and results-driven Software Developer & DevOps Engineer with a strong interest in building a clean and maintainable codebase. With hands-on experience across the full development lifecycle, from planning and design to implementation and deployment, I specialize in developing clean performant code and automating robust infrastructure workflows. I thrive at the intersection of software engineering and DevOps practices, where I can streamline processes, improve reliability, and accelerate delivery cycles. I’m always seeking new opportunities to impact meaningful projects, work with talented teams, and continue growing at the forefront of technology.

Frontend applications are growing in complexity. As teams scale and codebases expand, a single monolithic frontend becomes just as problematic as a monolithic backend, slow builds, deployment bottlenecks, and team conflicts over shared code.

Micro-frontends apply the same decomposition principles as microservices to the UI layer. And with Webpack Module Federation, implementing micro-frontends has never been more practical.

In this guide, you'll learn what micro-frontends are, when to use them, and how to set up Module Federation step-by-step, with real configuration examples you can apply to your own projects.

What Are Micro-Frontends?

A micro-frontend architecture breaks a web application into smaller, independently deployable UI units each owned by a separate team, built with its own toolchain, and composed together at runtime.

Think of it this way: instead of one giant React app that every team commits to, you have:

  • A shell app (host) that defines the layout and navigation.

  • Multiple remote apps (micro-frontends) that own specific features or pages.

Each remote can be developed, tested, and deployed independently without touching the host or other remotes.

When Should You Use Micro-Frontends?

Micro-frontends are best suited for:

  • Large teams working on the same product with minimal coordination overhead.

  • Multi-team organizations where different squads own distinct product areas.

  • Legacy modernization — incrementally replacing parts of an old app without a full rewrite.

  • Independent deployment cycles — when one team shouldn't be blocked by another's release schedule.

They're overkill for small teams or early-stage products. Start with a well-structured monorepo first.

What Is Webpack Module Federation?

Introduced in Webpack 5, Module Federation is a native plugin that allows JavaScript bundles to dynamically load code from other independently deployed applications at runtime.

Before Module Federation, sharing code across apps required publishing npm packages, a slow feedback loop. Module Federation eliminates that by letting apps expose and consume modules directly over the network.

Key concepts:

Term Description
Host The shell app that consumes remote modules
Remote An app that exposes modules to be consumed
Shared Dependencies shared across host and remotes to avoid duplication
Exposes The modules a remote makes available

Step 1 — Project Structure

For this guide, we'll build two applications:

  • shell — the host application

  • product-app — a remote micro-frontend exposing a ProductList component

/micro-frontend-demo
  /shell           ← Host app
  /product-app     ← Remote app

Each app is a standalone Webpack + React project. Initialize them separately:

mkdir shell product-app
cd shell && npm init -y && npm install webpack webpack-cli webpack-dev-server react react-dom
cd ../product-app && npm init -y && npm install webpack webpack-cli webpack-dev-server react react-dom

Step 2 — Configure the Remote App (product-app)

The remote app exposes components for the host to consume. Configure ModuleFederationPlugin in its webpack.config.js:

// product-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  mode: 'development',
  devServer: { port: 3001 },
  plugins: [
    new ModuleFederationPlugin({
      name: 'productApp',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/components/ProductList',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

Key properties explained:

  • name — unique identifier for this remote.

  • filename — the manifest file the host will fetch (remoteEntry.js).

  • exposes — maps public aliases to local module paths.

  • shared — prevents React from being loaded twice (critical for hooks to work correctly).

Step 3 — Configure the Host App (shell)

The shell app consumes the remote by referencing its remoteEntry.js URL:

// shell/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  mode: 'development',
  devServer: { port: 3000 },
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        productApp: 'productApp@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

The remote reference follows the pattern: name@url/remoteEntry.js

Step 4 — Consume the Remote Component

In your shell app, import the remote component using a dynamic import with React.lazy. This is required because Module Federation loads code asynchronously at runtime:

// shell/src/App.tsx
import React, { Suspense, lazy } from 'react';

const ProductList = lazy(() => import('productApp/ProductList'));

export default function App() {
  return (
    <div>
      <h1>My Store</h1>
      <Suspense fallback={<div>Loading products...</div>}>
        <ProductList />
      </Suspense>
    </div>
  );
}

When the shell renders <ProductList />, Webpack fetches the component's bundle from the product-app server at runtime — not at build time.

Step 5 — Sharing State Across Micro-Frontends

One of the trickiest aspects of micro-frontends is cross-app state management. A few proven approaches:

URL as State

Use the URL (query params, path segments) as the source of truth for shared state. It's universally accessible and framework-agnostic.

Custom Events (Web APIs)

Use the browser's native CustomEvent API for lightweight communication between micro-frontends:

// In product-app — dispatch an event
window.dispatchEvent(new CustomEvent('product:selected', { detail: { id: 42 } }));

// In shell — listen for the event
window.addEventListener('product:selected', (e) => {
  console.log('Selected product:', e.detail.id);
});

Shared State Library

If deeper integration is needed, expose a shared store (e.g., Zustand or a Redux slice) via Module Federation's shared config and consume it across remotes.

Step 6 — Deployment Considerations

In production, each micro-frontend is deployed independently to its own origin (CDN bucket, cloud function, or container). Update your remote URLs to use environment variables:

remotes: {
  productApp: `productApp@${process.env.PRODUCT_APP_URL}/remoteEntry.js`,
},

Deployment best practices:

  • Version your remote entries — use content-hashed filenames or versioned paths (/v2/remoteEntry.js) to prevent stale cache issues.

  • Health checks — monitor remoteEntry.js availability; a failed remote shouldn't crash the entire shell.

  • Fallback UI — always wrap remote imports in <Suspense> and error boundaries.

  • CI/CD independence — each remote should have its own pipeline with no dependency on the host's release cycle.

Common Pitfalls to Avoid

Duplicate dependencies: Forgetting to mark react and react-dom as singleton in shared config causes multiple React instances breaking hooks and context. Always enforce singletons for peer dependencies.

Tight coupling through contracts: If remotes depend on props or APIs defined by the host, you've recreated the monolith problem. Remotes should be self-contained and communicate via events or shared state patterns.

Over-decomposing the UI: Not every component needs to be a micro-frontend. Decompose at the page or feature level, not the component level.

Conclusion

Webpack Module Federation makes micro-frontends practical for production teams. By independently developing, deploying, and composing UI modules, you unlock true frontend scalability, shorter release cycles, clearer ownership, and the freedom to evolve each part of your UI without coordinating a full-app deployment.

Start with a single remote extracted from your monolith, validate the workflow end-to-end, and expand incrementally. The architecture rewards patience and deliberate boundaries.

Building micro-frontends with a different bundler like Vite or Rspack? Let me know in the comments, a follow-up guide might be on the way.

More from this blog

A

actocodes

27 posts