JavaScript Module Federation Explained
Understand Module Federation for micro-frontends -- share dependencies at runtime, load remote modules dynamically, and compose independent apps into one.
What you'll learn
- ✓What Module Federation is and the problems it solves
- ✓How to configure hosts and remotes with Webpack
- ✓Strategies for sharing dependencies and handling versioning
Prerequisites
- •JavaScript ES Modules
- •Basic Webpack configuration
- •Understanding of micro-frontend architecture
Module Federation is a Webpack 5 feature that lets multiple independently built and deployed JavaScript applications share code at runtime. Instead of bundling everything into a single monolithic build, each application exposes modules that other applications can consume — without npm packages, monorepos, or iframe hacks.
The problem Module Federation solves
In a traditional web application, all code is bundled together at build time. As the application grows, this creates problems:
- Long build times — every change rebuilds the entire app.
- Tight coupling — teams cannot deploy independently.
- Duplicate dependencies — multiple apps include their own copy of React, lodash, etc.
Micro-frontend architectures address these problems by splitting the frontend into independent applications. But they introduce a new challenge: how do you share code and dependencies between independently deployed apps without duplicating them?
Module Federation solves this by making dependency sharing a runtime concern rather than a build-time one.
Core concepts
Host and Remote
- Remote — an application that exposes modules for others to consume.
- Host — an application that consumes modules from remotes.
An application can be both a host and a remote simultaneously.
Shared dependencies
Module Federation lets applications declare which dependencies they want to share. At runtime, if two applications both need React 18, only one copy is loaded. If they need different major versions, both are loaded to maintain compatibility.
Setting up a remote
A remote application exposes specific modules through its Webpack configuration.
// webpack.config.js (remote: "catalog")
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
output: {
publicPath: "https://catalog.example.com/",
uniqueName: "catalog",
},
plugins: [
new ModuleFederationPlugin({
name: "catalog",
filename: "remoteEntry.js",
exposes: {
"./ProductList": "./src/components/ProductList",
"./ProductDetail": "./src/components/ProductDetail",
"./utils/pricing": "./src/utils/pricing",
},
shared: {
react: { singleton: true, requiredVersion: "^18.0.0" },
"react-dom": { singleton: true, requiredVersion: "^18.0.0" },
},
}),
],
};
The exposes field maps public module names to internal file paths. The filename specifies the entry point that hosts will load.
Setting up a host
The host application declares which remotes it wants to consume.
// webpack.config.js (host: "shell")
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "shell",
remotes: {
catalog: "catalog@https://catalog.example.com/remoteEntry.js",
},
shared: {
react: { singleton: true, requiredVersion: "^18.0.0" },
"react-dom": { singleton: true, requiredVersion: "^18.0.0" },
},
}),
],
};
Now the host can import modules from the remote as if they were local:
// In the host application
import ProductList from "catalog/ProductList";
import { calculateDiscount } from "catalog/utils/pricing";
function App() {
return (
<div>
<h1>Our Store</h1>
<ProductList onPriceCalc={calculateDiscount} />
</div>
);
}
Dynamic remotes
Hardcoding remote URLs in the Webpack config is limiting. Dynamic remotes let you load modules from URLs determined at runtime.
async function loadRemoteModule(remoteUrl, scope, module) {
// Load the remote entry script
await new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = remoteUrl;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
// Initialize the remote container
const container = window[scope];
await container.init(__webpack_share_scopes__.default);
// Get the module factory
const factory = await container.get(module);
return factory();
}
// Usage
const ProductList = await loadRemoteModule(
"https://catalog.example.com/remoteEntry.js",
"catalog",
"./ProductList"
);
This approach is powerful for A/B testing, feature flags, or loading different versions of a module based on user context.
Shared dependency strategies
The shared configuration controls how dependencies are deduplicated across applications.
Singleton
Forces a single instance of a library. Critical for libraries like React that break when multiple instances are loaded.
shared: {
react: {
singleton: true,
requiredVersion: "^18.0.0",
strictVersion: true, // Throw an error if versions are incompatible
},
}
Eager loading
By default, shared modules are loaded lazily. Set eager: true to include them in the initial bundle.
shared: {
lodash: {
eager: true, // Include in the initial bundle, don't lazy-load
},
}
Version ranges
shared: {
axios: {
requiredVersion: "^1.4.0",
// If the host provides axios 1.5.0, use that
// If the host provides axios 0.27.0, load our own copy
},
}
Error handling and fallbacks
Remote modules can fail to load — the server might be down, or a network error might occur. Always handle this gracefully.
// React example with error boundary and lazy loading
import React, { Suspense, lazy } from "react";
const RemoteProductList = lazy(() =>
import("catalog/ProductList").catch(() => {
// Fallback to a local component if the remote fails
return import("./FallbackProductList");
})
);
function App() {
return (
<ErrorBoundary fallback={<div>Something went wrong.</div>}>
<Suspense fallback={<div>Loading products...</div>}>
<RemoteProductList />
</Suspense>
</ErrorBoundary>
);
}
Framework-agnostic usage
Module Federation is not limited to React. Any JavaScript module can be federated.
// Remote: exposes a vanilla JS utility
// webpack.config.js
exposes: {
"./analytics": "./src/analytics.js",
}
// analytics.js
export function trackEvent(name, data) {
fetch("/api/events", {
method: "POST",
body: JSON.stringify({ name, data, timestamp: Date.now() }),
});
}
// Host: consumes it in any framework
import { trackEvent } from "analytics/analytics";
trackEvent("page_view", { page: "/home" });
Beyond Webpack: Module Federation 2.0
The original Module Federation was tightly coupled to Webpack. Module Federation 2.0 (via the @module-federation/enhanced package) adds:
- Framework support — official plugins for Rspack, Vite, and others.
- Type safety — automatic TypeScript type generation for remote modules.
- Runtime API — programmatic control over loading and versioning.
// vite.config.js with Module Federation 2.0
import { federation } from "@module-federation/vite";
export default {
plugins: [
federation({
name: "host",
remotes: {
catalog: {
type: "module",
name: "catalog",
entry: "https://catalog.example.com/remoteEntry.js",
},
},
shared: ["react", "react-dom"],
}),
],
};
When to use Module Federation
Module Federation is a good fit when:
- Multiple teams work on different parts of the frontend and need independent deployment.
- Shared UI libraries need to be updated without rebuilding all consumers.
- Large applications have long build times that can be reduced by splitting into smaller units.
- A/B testing requires loading different component versions at runtime.
It is likely overkill for small projects with a single team or when a simple npm package would suffice.
Summary
Module Federation enables runtime code sharing between independently deployed JavaScript applications:
- Remotes expose modules; hosts consume them via standard
importsyntax. - Shared dependencies are deduplicated at runtime, avoiding duplicate copies of React or other libraries.
- Dynamic remotes let you load modules from URLs determined at runtime.
- Error boundaries and fallbacks are essential for production resilience.
- Module Federation 2.0 extends support beyond Webpack to Vite, Rspack, and other bundlers.
It is the most practical solution for micro-frontend architectures that need to share code without the complexity of iframes or the coupling of monorepos.
Related articles
- JavaScript JavaScript Design Patterns Every Developer Should Know
Learn the most useful JavaScript design patterns with practical examples including Singleton, Observer, Factory, Strategy, and more for cleaner code.
- JavaScript The Decorator Pattern in JavaScript
Learn the Decorator pattern in JavaScript -- wrap functions and classes to add logging, caching, validation, and retry logic without modifying original code.
- JavaScript JavaScript Event Loop Explained: How Async Code Really Works
Understand how the JavaScript event loop handles async operations including the call stack, microtasks, macrotasks, and execution order with practical examples.
- JavaScript JavaScript structuredClone: The Modern Deep Copy Solution
Learn how to use JavaScript structuredClone for deep copying objects, when it beats JSON.parse(JSON.stringify()), and what types it supports and cannot handle.