Module Federation: Sharing Code Between Applications with Webpack

Table of Contents
Big thanks to our contributors those make our blogs possible.

Our growing community of contributors bring their unique insights from around the world to power our blog. 

Introduction

As frontend applications grow in scale and teams multiply, monolithic builds become unwieldy: long compile times, large bundles, and tight coupling between independently deployable features. Webpack Module Federation addresses these pain points by enabling runtime code sharing across separately built and deployed applications. Rather than bundling every shared component or utility into each consumer’s build, hosts load modules dynamically from remotes on demand—driving:

  • Independent Deployability: Teams deploy micro‑frontends without coordinating full‑app releases.
  • Bundle Size Reduction: Shared code lives in one place; hosts only fetch what they need.
  • Version Safety: Shared dependencies (e.g., React) load once as singletons, preventing mismatches.
  • Runtime Flexibility: Hosts can switch remote versions via configuration, feature‑flag remotes, or A/B test entire micro‑frontends.

In this in‑depth guide, we’ll cover:

  1. Module Federation Basics: Key concepts and terminology
  2. Hands‑On Example: Building a feature-app (remote) and app-shell (host) from scratch
  3. Advanced Configuration: Shared dependency strategies, eager vs. lazy loading, fallback remotes
  4. Dynamic Remotes: Installing remote URLs at runtime and environment‑driven deployments
  5. Performance and Caching: Lazy loading, prefetching, and caching considerations
  6. Error Handling & Resilience: Health checks, timeouts, and graceful degradation
  7. Security Implications: CORS, code integrity, and sandboxing
  8. CI/CD & Testing Strategies: Verifying federation boundaries in pipelines
  9. Best Practices & Anti‑Patterns: Organizing exposes, versioning, and maintaining a healthy federation

By the end, you’ll have a blueprint for powering your micro‑frontend architecture with Module Federation—maximizing reuse, minimizing duplication, and scaling team autonomy.

1. Module Federation Basics

1.1 Core Terminology

TermDescription
HostThe “shell” app that consumes federated modules at runtime.
RemoteAn independently built app that exposes modules via a remoteEntry.js manifest.
ExposesA mapping in a remote’s Webpack config: local source paths → federated module names (e.g., ./Widget).
RemotesA mapping in a host’s Webpack config: federated names → <global>@<URL>/remoteEntry.js.
SharedDependencies declared in both host & remote to ensure a singleton instance (e.g., React, lodash).

1.2 Runtime Flow

  1. Bootstrap: The host’s main bundle loads in the browser.
  2. Discovery: When code calls import('remoteApp/Module'), Webpack fetches remoteEntry.js from the remote URL.
  3. Resolution: Using the manifest, Webpack resolves and executes chunk files for that module.
  4. Sharing: If the module depends on shared libraries, Webpack loads or reuses a single instance—avoiding duplicates.

A simplified diagram:

pgsqlCopyEdit[ Host Bundle ]   -> import('remoteA/Button')
                 \
                  fetch http://remoteA/remoteEntry.js 
                    -> manifest with mapping
                  fetch chunks for Button
                  mount Button component in Host

2. Hands‑On Example: Remote and Host Setup

We’ll build two CRA-based React apps: feature-app (remote) on port 3001, and app-shell (host) on port 3000.

2.1 Initialize Projects

bashCopyEdit# Feature app (remote)
npx create-react-app feature-app
cd feature-app
npm install webpack@5 webpack-cli@4

# Shell app (host)
cd ..
npx create-react-app app-shell
cd app-shell
npm install webpack@5 webpack-cli@4

2.2 Remote Configuration (feature-app)

Create or extend webpack.config.js in feature-app/:

jsCopyEditconst { override, addWebpackPlugin } = require('customize-cra');
const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin;

module.exports = override(
  addWebpackPlugin(
    new ModuleFederationPlugin({
      name: 'featureApp',
      filename: 'remoteEntry.js',
      exposes: {
        './FeatureWidget': './src/FeatureWidget'
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
      }
    })
  )
);

FeatureWidget Component (src/FeatureWidget.js):

jsxCopyEditimport React from 'react';

export default function FeatureWidget() {
  return (
    <div style={{
      border: '2px dashed #007acc',
      padding: '1rem',
      margin: '1rem 0'
    }}>
      <h2>Federated Feature Widget</h2>
      <p>This was loaded dynamically from another app!</p>
    </div>
  );
}

Expose it by modifying your app’s start script for port:

jsoncCopyEdit// package.json
"scripts": {
  "start": "PORT=3001 react-scripts start",
  // ...
}

Run:

bashCopyEditcd feature-app
npm start

A remoteEntry.js file will now be served at http://localhost:3001/remoteEntry.js.

2.3 Host Configuration (app-shell)

In app-shell/ webpack.config.js:

jsCopyEditconst { override, addWebpackPlugin } = require('customize-cra');
const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin;

module.exports = override(
  addWebpackPlugin(
    new ModuleFederationPlugin({
      name: 'appShell',
      remotes: {
        featureApp: 'featureApp@http://localhost:3001/remoteEntry.js'
      },
      shared: {
        react: { singleton: true, eager: false, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, eager: false, requiredVersion: '^18.0.0' }
      }
    })
  )
);

Consume the remote component in src/App.js:

jsxCopyEditimport React, { Suspense, lazy } from 'react';

const FeatureWidget = lazy(() => import('featureApp/FeatureWidget'));

function App() {
  return (
    <div>
      <h1>Application Shell</h1>
      <p>This is the host app.</p>
      <Suspense fallback={<div>Loading feature…</div>}>
        <FeatureWidget />
      </Suspense>
    </div>
  );
}

export default App;

Start the shell:

bashCopyEditcd app-shell
npm start

Visit http://localhost:3000: you should see content from both the shell and your federated feature.

3. Advanced Configuration

3.1 Shared Dependency Strategies

OptionBehavior
singletonEnsures only one version is loaded across host & remotes.
eagerWhen true, loads the shared module upfront (in initial chunk) rather than lazily on demand.
requiredVersionThrows a warning or error if versions don’t overlap semantically.

Example: Eager Singleton React

jsCopyEditshared: {
  react: { singleton: true, eager: true, requiredVersion: '^18.0.0' },
  'react-dom': { singleton: true, eager: true, requiredVersion: '^18.0.0' }
}

Use eager: true when you need shared modules to be available synchronously (e.g., for hooks or context).

3.2 Handling Version Mismatches

If a remote was built with React ^17.0.0 but your host uses ^18.0.0, you may:

  1. Update remote to match host.
  2. Relax requiredVersion to allow ^17 || ^18.
  3. Force host version by eager: true, so host provides React to remotes.
jsCopyEditreact: {
  singleton: true,
  eager: true,
  requiredVersion: '^17.0.0 || ^18.0.0'
}

3.3 Multiple Remotes

Hosts can consume several remotes:

jsCopyEditremotes: {
  featureApp: 'featureApp@https://cdn.example.com/featureApp/remoteEntry.js',
  utilsLib:  'utilsLib@https://cdn.example.com/utilsLib/remoteEntry.js'
}

Import as needed:

jsCopyEditconst Util = lazy(() => import('utilsLib/UtilityFunction'));

4. Dynamic Remotes & Environment‑Driven URLs

Hard‑coding remote URLs ties your builds to one environment. Instead, use dynamic remotes:

jsCopyEditnew ModuleFederationPlugin({
  name: 'appShell',
  remotes: {
    featureApp: `featureApp@[window.featureAppUrl]/remoteEntry.js`
  },
  // ...
});

Inject window.featureAppUrl in your HTML template or via a small runtime script:

htmlCopyEdit<!DOCTYPE html>
<html>
<head>
  <!-- ... -->
  <script>
    window.featureAppUrl = process.env.FEATURE_APP_URL || 'https://cdn.example.com/featureApp';
  </script>
</head>
<body>
  <div id="root"></div>
  <script src="main.js"></script>
</body>
</html>

Set FEATURE_APP_URL per environment (staging, prod) without rebuilding the host.

5. Performance & Caching Considerations

5.1 Lazy Loading & Prefetching

By default, remotes load on demand via import(). You can also prefetch to improve UX:

jsxCopyEditimport('featureApp/FeatureWidget' /* webpackPrefetch: true */);

This hints the browser to fetch chunks during idle time.

5.2 Cache‑Busting

RemoteEntry and chunks should be fingerprinted in production for cache invalidation:

  • Configure your build to emit remoteEntry.[contenthash].js.
  • Update host’s remotes mapping via a manifest or dynamic lookup.

5.3 Concurrent Requests

Avoid performance cliffs by exposing too many small modules. Instead:

  • Bundle related features into one federated entry (e.g., ./UIComponents).
  • Control chunk sizes with webpack’s optimization.splitChunks in the remote.

6. Error Handling & Resilience

6.1 Health‑Check Fallback

Wrap imports to catch load failures:

jsxCopyEditasync function loadWidget() {
  try {
    const { default: Widget } = await import('featureApp/FeatureWidget');
    return Widget;
  } catch (e) {
    console.error('Failed to load federated widget', e);
    return null; // or return a fallback component
  }
}

const FeatureWidget = React.lazy(loadWidget);

6.2 Timeouts

You can implement a timeout wrapper:

jsxCopyEditfunction withTimeout(promise, ms) {
  let id;
  const timeout = new Promise((_, reject) =>
    id = setTimeout(() => reject(new Error('Load timeout')), ms)
  );
  return Promise.race([promise, timeout]).finally(() => clearTimeout(id));
}

const FeatureWidget = React.lazy(() =>
  withTimeout(import('featureApp/FeatureWidget'), 5000)
);

If the remote is slow or unreachable, you degrade gracefully.

7. Security Implications

7.1 CORS Configuration

Ensure the remote’s server serves remoteEntry.js with:

makefileCopyEditAccess-Control-Allow-Origin: *

or restrict to specific hosts for tighter security.

7.2 Code Integrity

Consider using Subresource Integrity (SRI) for remoteEntry:

htmlCopyEdit<script
  src="https://cdn/.../remoteEntry.js"
  integrity="sha384-abc123..."
  crossorigin="anonymous">
</script>

SRI prevents malicious tampering of remote scripts.

7.3 Sandboxing

Federated code runs in the host’s JS context. Avoid remote code having elevated privileges or access to host internals unless explicitly intended.

8. CI/CD & Testing Strategies

8.1 Automated Federation Tests

  • Spin up host and remote locally in your CI job.
  • Run a smoke test that renders the host page and verifies remote components appear.
  • Use Puppeteer or Playwright to check for console errors, missing network requests, and correct rendering.

8.2 Version Contract Checks

  • CI step to compare host’s expected remoteEntry version (e.g., from manifest) against the remote’s deployed version endpoint.
  • Fail the build if the remote API contract is out of sync.

8.3 Canary Releases

  • Deploy a new remote version behind a feature flag or alternate remote name (e.g., featureAppBeta).
  • Allow a percentage of hosts to consume the beta remote by toggling which remote URL they load.

9. Best Practices & Anti‑Patterns

Best PracticeAnti‑Pattern
Group related modules under one expose (e.g., ./UI)Exposing dozens of tiny modules individually
Use semantic versioning for remotes and update hosts via manifestHard‑coding remote version in host build
Document federated APIs and shared contractsImplicit dependencies and undocumented expects
Health checks and fallbacksLetting load failures crash the entire app
Automate CI federation testsManual checks prone to oversight

Conclusion

Webpack Module Federation provides a powerful framework for runtime code sharing—unlocking true micro‑frontend and distributed team architectures. By configuring remotes, exposes, and shared dependencies, you achieve:

  • Independent deployments: Remote apps roll out features on their own schedules.
  • Smaller host bundles: Only load code you need, when you need it.
  • Singletons: No duplicate React or utility libraries.
  • Dynamic routing: Switch remote versions without host rebuilds.
  • Resilience: Fallbacks and timeouts keep your app stable even when remotes fail.

Start by federating a small widget, then expand to shared libraries, utility modules, and entire feature sets. Couple federation with robust CI tests, semantic versioning, and health‑check fallbacks, and you’ll have a scalable, maintainable micro‑frontend ecosystem that grows with your teams and user needs

Let's connect on TikTok

Join our newsletter to stay updated

Sydney Based Software Solutions Professional who is crafting exceptional systems and applications to solve a diverse range of problems for the past 10 years.

Share the Post

Related Posts