web_dev

**How to Build Scalable Web Apps Using Webpack 5 Module Federation**

Learn how Webpack 5's Module Federation transforms micro-frontends by enabling runtime code sharing between independent apps. Build scalable, maintainable web applications with our complete setup guide.

**How to Build Scalable Web Apps Using Webpack 5 Module Federation**

Building a large web application often feels like trying to organize a city where every neighborhood speaks a different language. Each team builds its own section, and before you know it, you have duplicate code everywhere, massive bundles, and a tangled mess of dependencies. I’ve been there. The promise of micro-frontends—breaking that giant, monolithic interface into smaller, independent apps—is incredibly appealing. But for years, sharing code between these separate pieces was clumsy, often involving package publishing or copying files, which defeated the purpose of independent deployment.

Then Webpack 5 introduced something that changed the game: Module Federation. It’s a native way for one built and deployed application to use parts of another at runtime. Think of it as a dynamic linking system for the web. Your user’s browser becomes the integration point, pulling in components, utilities, or libraries from different sources on the fly. This means no more giant, all-in-one bundles, and teams can update their parts without forcing a rebuild of the entire world.

Let me show you how this works from the ground up. The core of everything is a simple change in your Webpack configuration. You define two roles: a “host” that consumes code, and a “remote” that provides it. A module can be both.

Here’s a remote application, like a shared design system, that wants to expose a Button and a Card component for others to use.

// webpack.config.js for the 'design_system' remote
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  // ... your standard webpack config (mode, entry, output)
  plugins: [
    new ModuleFederationPlugin({
      name: 'design_system', // A unique identifier for this app
      filename: 'remoteEntry.js', // The manifest file webpack creates
      exposes: {
        // Define what you want to share
        './Button': './src/components/Button.jsx',
        './Card': './src/components/Card.jsx',
        './theme': './src/theme/index.js'
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
      }
    })
  ]
};

When you build this, Webpack creates a remoteEntry.js file. This isn’t your whole app bundle; it’s a small runtime manifest that tells other applications, “Here are the modules I have available for you to load.” Now, let’s look at a host application that wants to use that Button.

// webpack.config.js for the 'product_app' host
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  // ... other webpack config
  plugins: [
    new ModuleFederationPlugin({
      name: 'product_app',
      remotes: {
        // Map a friendly name to that remote's manifest file
        ds: 'design_system@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
      }
    })
  ]
};

The magic word here is remotes. I’m telling my host app, “Hey, there’s a remote named design_system, and you can find its entry point at this URL.” The shared section is equally important. It tells Webpack, “These libraries should be shared. If the remote already loaded React, use that same instance. Don’t load it twice.” The singleton: true enforces this, which is crucial for libraries like React that get unhappy if there are multiple copies on the page.

Now, in my host application’s React code, I can use that remote Button as if it were a local file.

// In product_app/src/ProductPage.jsx
import React, { Suspense, lazy } from 'react';

// Use a dynamic import pointing to the remote
const RemoteButton = lazy(() => import('ds/Button'));

function ProductPage() {
  return (
    <div>
      <h1>Our Featured Product</h1>
      <Suspense fallback={<div>Loading button from design system...</div>}>
        <RemoteButton onClick={() => alert('Added!')}>
          Buy Now
        </RemoteButton>
      </Suspense>
    </div>
  );
}

This is the key moment. When this code runs, the host application dynamically fetches the ds/Button module from the remote server at http://localhost:3001. It only loads the code for the Button, not the entire design system app. The Suspense boundary is necessary because this is a network request; we need to show a loading state while the code is being fetched.

This pattern changes how you think about dependencies. You can share more than just UI components. Let’s say you have a set of authentication utilities managed by a separate team.

// In an 'auth' remote's webpack config
new ModuleFederationPlugin({
  name: 'auth',
  filename: 'remoteEntry.js',
  exposes: {
    './utils': './src/authUtils.js', // Expose functions
    './LoginForm': './src/components/LoginForm.jsx' // Expose a component
  },
  shared: { /* shared dependencies */ }
});

// In authUtils.js
export const validatePassword = (pw) => { /* complex logic */ };
export const formatUserProfile = (user) => { /* formatting logic */ };

// In your host app, using the utility
import('auth/utils').then(({ validatePassword }) => {
  const isValid = validatePassword(userInput);
  console.log(isValid);
});

One of the trickiest parts is managing state. How does a component from the design system remote interact with the user state managed in the host app? You don’t want tight coupling. A common approach is to create a dedicated remote for shared state or use patterns like passing props or using a global event system.

For instance, you could have a shared_state remote that exposes a lightweight state store.

// In a shared_state remote
// src/store.js
import { createStore } from 'redux';

const initialState = { user: null, notifications: [] };
const store = createStore(reducer, initialState);

export const getSharedState = store.getState;
export const dispatchSharedAction = store.dispatch;
export const subscribeToSharedState = store.subscribe;

// Expose it in webpack config: './store': './src/store.js'

// In a host app component
import React, { useEffect, useState } from 'react';

function UserHeader() {
  const [sharedState, setSharedState] = useState({});

  useEffect(() => {
    // Dynamically load the shared state module
    import('shared_state/store').then(({ getSharedState, subscribeToSharedState }) => {
      setSharedState(getSharedState()); // Get initial state

      // Subscribe to future changes
      const unsubscribe = subscribeToSharedState(() => {
        setSharedState(getSharedState());
      });

      return unsubscribe; // Cleanup on unmount
    });
  }, []);

  return <header>Hello, {sharedState.user?.name}</header>;
}

This keeps your applications loosely coupled. The host app doesn’t need to know if the store is Redux, Zustand, or a simple event emitter; it just imports and uses the agreed-upon interface.

Development gets interesting. You now have multiple applications running on different ports. I find it easiest to use a tool like concurrently to start them all together.

// In your root package.json
{
  "scripts": {
    "dev": "concurrently \"npm run dev:host\" \"npm run dev:design\" \"npm run dev:auth\"",
    "dev:host": "webpack serve --config webpack.host.js --port 8080",
    "dev:design": "webpack serve --config webpack.design.js --port 8081",
    "dev:auth": "webpack serve --config webpack.auth.js --port 8082"
  }
}

You also need to configure your development servers to allow these cross-origin requests. It’s a simple header setting.

// Inside your webpack devServer config for the design system (port 8081)
devServer: {
  port: 8081,
  headers: {
    "Access-Control-Allow-Origin": "*", // Allows requests from the host app on port 8080
  },
  // Hot Module Replacement (HMR) still works!
}

When it’s time to go to production, you face new questions. Where do you host these remote entry files? How do you handle version updates? The remote URL in your host configuration shouldn’t be hardcoded to localhost. You can make it configurable.

// A more dynamic host configuration
new ModuleFederationPlugin({
  name: 'product_app',
  remotes: {
    ds: `design_system@${process.env.DESIGN_SYSTEM_URL}/remoteEntry.js`,
  },
  shared: { /* ... */ }
});

Then, you can set the DESIGN_SYSTEM_URL environment variable in your build pipeline to point to a CDN URL, like https://assets.yourcompany.com/design-system/v1.2.0/. This also gives you a clear versioning story. You can update the remote independently, and host apps will use the new version the next time they load the entry file.

You must plan for failure. What if the remote server is down? Using React’s Error Boundaries is a good practice to catch failed dynamic imports and show a friendly message or a fallback UI.

class RemoteModuleErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render shows the fallback UI.
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <div>Could not load the required component. Please try again later.</div>;
    }

    return this.props.children;
  }
}

// Using it in your app
function App() {
  return (
    <RemoteModuleErrorBoundary>
      <Suspense fallback={<Spinner />}>
        <ProductPage /> {/* Contains lazy remote imports */}
      </Suspense>
    </RemoteModuleErrorBoundary>
  );
}

Performance is a major benefit, but you need to be smart. You don’t want to fetch a remote module only when a user clicks a button if that will cause a visible delay. Webpack allows you to add prefetch hints.

// Instead of just a dynamic import, you can prefetch during idle time
const RemoteButton = lazy(() => import(/* webpackPrefetch: true */ 'ds/Button'));

The browser will load the remoteEntry.js for the design system early, and then fetch the Button chunk when it has spare bandwidth, making the actual user interaction feel instantaneous.

Getting started can feel like a lot, but the payoff is huge. You enable teams to own their code from development to deployment, all while presenting a single, seamless application to your user. Bundle sizes shrink because shared libraries aren’t duplicated. Caching improves because a common library update only requires downloading one new shared chunk, not rebuilding every app. It finally makes the dream of independent, collaborative frontend development a practical reality.

Start small. Take a single, well-defined component or utility function and try to federate it. Once you see that code loading from another build into your app, the pieces will click into place. You’re not just sharing code; you’re building a connected ecosystem.

Keywords: webpack module federation, micro frontends, webpack 5 module federation, micro frontend architecture, webpack configuration, dynamic module loading, module federation tutorial, webpack remote modules, micro frontend development, webpack host application, webpack remote application, module federation setup, webpack shared dependencies, micro frontend patterns, webpack runtime module sharing, module federation example, webpack federated modules, micro frontend state management, webpack module federation guide, dynamic import webpack, webpack module federation performance, micro frontend deployment, webpack remote entry, module federation best practices, webpack singleton modules, micro frontend code sharing, webpack federation plugin, module federation development, webpack cross-origin modules, micro frontend webpack config, webpack module federation production, federated architecture, webpack module loading, micro frontend integration, webpack shared libraries, module federation error handling, webpack lazy loading modules, micro frontend routing, webpack module federation cdn, dynamic component loading, webpack module federation versioning, micro frontend build optimization, webpack module federation caching, federated components, webpack module federation debugging, micro frontend team collaboration, webpack module federation security, federated module architecture, webpack module federation testing, micro frontend scalability, webpack hot reload federation



Similar Posts
Blog Image
Is Kubernetes the Secret Sauce for Modern IT Infrastructure?

Revolutionizing IT Infrastructure: The Kubernetes Era

Blog Image
WebAssembly Interface Types: The Secret Weapon for Multilingual Web Apps

WebAssembly Interface Types enable seamless integration of multiple programming languages in web apps. They act as universal translators, allowing modules in different languages to communicate effortlessly. This technology simplifies building complex, multi-language web applications, enhancing performance and flexibility. It opens up new possibilities for web development, combining the strengths of various languages within a single application.

Blog Image
Rust's Async Trait Methods: Game-Changing Power for Flexible Code

Explore Rust's async trait methods: Simplify flexible, reusable async interfaces. Learn to create powerful, efficient async systems with improved code structure and composition.

Blog Image
**JWT Authentication Security Guide: Refresh Token Rotation and Production-Ready Implementation**

Learn how to build secure JWT authentication with refresh token rotation, automatic token handling, and protection against replay attacks. Implement production-ready auth systems.

Blog Image
Boost Website Performance with Intersection Observer API: Lazy Loading Techniques

Optimize web performance with the Intersection Observer API. Learn how to implement lazy loading, infinite scroll, and viewport animations while reducing load times by up to 40%. Code examples included. Try it now!

Blog Image
JAMstack Optimization: 10 Proven Strategies for Building High-Performance Web Apps

Discover practical JAMstack strategies for building faster, more secure websites. Learn how to implement serverless functions, authentication, and content management for high-performance web applications. Click for expert tips.