Webpack Module Federation — Complete Guide
Your Webpack 5 configuration handles entry points, loaders, and dev server setup. But the feature that makes micro frontends possible with Webpack is Module Federation — a Webpack 5 plugin that lets multiple independent builds share code at runtime without bundling them together.
This article covers how Module Federation works in a real-world micro frontend monorepo — host configuration, remote configuration, exposed modules, shared dependencies with the singleton pattern, how remoteEntry.js works under the hood, and the critical differences between local development and production setups.
This is Article 12 in the Micro Frontend Architecture series. If you haven't set up your Webpack 5 config yet, start with Webpack 5 Configuration for Micro Frontends.

What is Webpack Module Federation?
Module Federation is a built-in Webpack 5 plugin (opens in a new tab) that allows separately built applications to share JavaScript modules at runtime. Instead of packaging everything into a single monolithic bundle, each micro frontend builds independently and exposes specific components that other apps can consume.
The key insight: Module Federation does not copy code between builds. It creates a runtime contract where one build can request modules from another build — and both builds negotiate which shared dependencies to use so nothing is duplicated in the browser.
Three core concepts make this work:
- Host — the main application shell that loads and orchestrates remote modules
- Remote — an independently deployed app that exposes modules for the host to consume
- Shared — dependencies that both host and remote agree to share at runtime instead of bundling separately
Module Federation vs other integration patterns. Unlike iframe-based or build-time integration, Module Federation shares actual JavaScript modules at runtime. Components from different MFEs render in the same DOM, share the same React instance, and can access the same Redux store. For a comparison of all patterns, see 5 Micro Frontend Integration Patterns.
Host Configuration — The Main App Shell
The host application defines which remotes it wants to consume and where to find them. The remotes object maps each MFE name to its remoteEntry.js URL — and this URL is completely different between local development and production.
What Changes Between Local and Production
| Config Property | Local Development | Production / Server |
|---|---|---|
mode | development | production |
publicPath | https://localhost:PORT/ | /mfe-name/ |
| Remote URL (host) | MFE@https://localhost:PORT/remoteEntry.js | MFE@/mfe-name/remoteEntry.js |
devServer | Enabled (HTTPS + CORS) | Not included |
splitChunks | false | Enabled (vendor splitting) |
moduleIds | Not set (defaults to named) | deterministic |
runtimeChunk | Not set | single |
filename | [name].bundle.js | [name].bundle.js |
| HTTPS | Self-signed certs | Handled by reverse proxy |
| CORS headers | Access-Control-Allow-Origin: * | Handled by reverse proxy |
The biggest difference is the remote URLs. In local development, each MFE runs on a separate port (https://localhost:4002). In production, all MFEs are served from the same domain through a reverse proxy that routes /products/ to the Products MFE's built assets.
publicPath must match the remote URL. If the host expects the Products remote at /products/remoteEntry.js, then the Products MFE's output.publicPath must be /products/. A mismatch causes chunk loading errors — the remote loads remoteEntry.js correctly but then requests chunks from the wrong path.
Remote Configuration — Exposing Modules
Each remote MFE uses the same ModuleFederationPlugin but with different options. Instead of defining remotes, it defines exposes — a map of module paths that the host can import.
How Exposes Works
The exposes object creates a mapping between external module names and internal file paths. When the host imports Products/ProductList, Webpack resolves it to ./src/components/ProductList inside the Products remote.
Naming Rules for Module Federation
All Exposed Modules in the Monorepo
Each remote MFE exposes specific components that the host loads on demand. Here is the complete mapping:
| MFE Name | Exposed Modules | Port (Local) |
|---|---|---|
| Products | ./ProductList, ./ProductCatalog, ./ProductImages, ./BulkUpload, ./SingleUpload | 4002 |
| Inventory | ./InventoryDashboard, ./BuyingDashboard, ./PurchaseOrders, ./Suppliers | 4003 |
| Orders | ./OrdersDashboard, ./OrderManagement, ./Returns, ./Refunds, ./Shipping | 4004 |
| Onboarding | ./OnboardingApp | 4001 |
| Pricing | ./PricingDashboard | 4005 |
| Earnings | ./EarningsDashboard | 4006 |
| Analytics | ./AnalyticsDashboard | 4007 |
| Reports | ./ReportsDashboard | 4008 |
| Settings | ./SettingsDashboard | 4009 |
| Support | ./SupportDashboard, ./Tickets, ./TicketDetail, ./Chat, ./HelpCenter | 4010 |
| Reviews | ./ReviewsDashboard | 4011 |

Each remote runs on its own port locally. Port 4000 is reserved for the host shell. Remotes use ports 4001-4011. In production, ports don't exist — the reverse proxy maps URL paths to each MFE's static assets.
Shared Dependencies — The Singleton Pattern
Shared dependencies are the most critical part of Module Federation configuration. Without proper sharing, each MFE bundles its own copy of React, Redux, and other libraries — causing crashes, bloated bundles, and subtle bugs.
Why Each Dependency Is Shared
| Dependency | singleton | requiredVersion | Why Shared |
|---|---|---|---|
react | true | ^18.3.1 | Multiple React copies cause hooks to crash |
react-dom | true | ^18.3.1 | Must match react version exactly |
react-router-dom | true | ^7.1.5 | One router manages all MFE routes |
@reduxjs/toolkit | true | ^2.6.0 | One Redux store across all MFEs |
react-redux | true | ^9.2.0 | Provider must wrap entire app once |
@myapp/store | true | 0.0.1 | Shared monorepo package — same store instance |
What Happens Without singleton: true
The singleton: true flag is non-negotiable for React. Without it, the host and remote each instantiate their own React module. React hooks maintain state in a module-level variable — two modules mean two separate state trees, which immediately breaks useState, useEffect, and every other hook.
Version Mismatch Behavior
requiredVersion is a safety net, not enforcement. When singleton: true, the first-loaded version wins regardless of requiredVersion. The version check only generates console warnings (or errors with strictVersion: true) — it does not prevent the app from loading. Keep all MFEs on the same version in your monorepo's root package.json to avoid these warnings entirely.
How remoteEntry.js Works
The filename: "remoteEntry.js" option in the remote config generates a small manifest file. This file is the entry point that the host uses to access the remote's exposed modules. Understanding this flow is essential for debugging.
The key insight: remoteEntry.js is not the remote's entire bundle. It is a lightweight manifest that:
- Registers a global container (
window.Products,window.Orders, etc.) - Lists all exposed modules and their chunk files
- Lists all shared dependencies and their version requirements
- Provides a
get()function that the host calls to retrieve specific modules
When the host calls import("Products/ProductList"), Webpack's runtime:
- Calls
window.Products.get("./ProductList") - The container checks if shared deps are already loaded (singleton negotiation)
- Downloads only the chunk containing the
ProductListcomponent - Returns the module to
React.lazy()for rendering
Loading Remote Modules in the Host
The host uses React.lazy() and Suspense to load remote components on demand. This is the same pattern you'd use for code splitting in a regular React app — Module Federation makes it work across build boundaries.
When a user navigates to /products, React.lazy triggers a dynamic import. Webpack intercepts import("Products/ProductList"), fetches the Products remote's remoteEntry.js (if not already loaded), negotiates shared deps, downloads the component chunk, and renders it — all transparently.
The Bootstrap Pattern — Why Your Entry Point Must Be Async
Module Federation requires an async boundary before any shared module is used. Without it, shared dependencies are consumed before Module Federation has a chance to negotiate which versions to use.
The import("./bootstrap") call creates an async chunk. Webpack uses this boundary to:
- Load
remoteEntry.jsfiles for all configured remotes - Compare shared dependency versions between host and remotes
- Decide which copy of each shared dependency to use
- Then execute
bootstrap.jswith all shared deps resolved
Every MFE needs this pattern — not just the host. If a remote MFE has its own HTML page for standalone development, it must also use the bootstrap pattern. Otherwise, running the remote standalone will crash with "Shared module is not available for eager consumption."
Common Errors and Solutions
Debugging Checklist
| Symptom | Check | Fix |
|---|---|---|
| ScriptExternalLoadError | Is the remote dev server running? | Start the remote on the correct port |
| Module not found in container | Does the exposes key match the import? | Ensure ./ prefix in exposes, no ./ in import |
| Invalid hook call | Is React configured as singleton? | Add singleton: true to both host and remote |
| Chunks return 404 | Does publicPath match the serving path? | Local: https://localhost:PORT/, Prod: /mfe-name/ |
| Styles missing from remote | Is MiniCssExtractPlugin configured? | See Tailwind CSS in MFE Monorepo |
| Eager consumption error | Is the bootstrap pattern used? | Split index.js into index.js + bootstrap.js |
Module Federation vs NextFederationPlugin
This guide covers Module Federation for React MFEs using webpack.container.ModuleFederationPlugin. If your host or remotes use Next.js, the setup is different — Next.js requires NextFederationPlugin from @module-federation/nextjs-mf, which handles SSR, different remote entry paths (_next/static/chunks/remoteEntry.js), and Next.js-specific configuration like basePath and assetPrefix. The Next.js Module Federation setup is covered in a dedicated article later in this series.
What's Next?
← Back to Tailwind CSS in Micro Frontend Monorepo
Continue to Host and Remote Apps in Module Federation →
Frequently Asked Questions
What is Webpack Module Federation?
Webpack Module Federation is a Webpack 5 feature that allows multiple independent builds to share code at runtime. Each build can expose modules that other builds consume — without bundling them together at build time. This is the core mechanism that makes micro frontend architecture work with Webpack.
What is the difference between host and remote in Module Federation?
The host is the main application shell that loads and renders remote modules. It defines which remotes to consume and at what URLs. The remote is an independently deployed application that exposes specific modules (components, utilities, pages) for the host to consume. A single app can be both a host and a remote simultaneously.
Why do I need singleton: true for React in shared dependencies?
Without singleton: true, each MFE loads its own copy of React. Multiple React instances cause the "Invalid hook call" error because React hooks rely on a single internal state tree. Setting singleton: true forces all MFEs to use the same React instance — the one loaded first by the host.
What is remoteEntry.js and how does it work?
remoteEntry.js is a manifest file generated by Module Federation's filename option. It registers a global container (e.g., window.Products) that the host uses to request exposed modules. When the host calls import('Products/ProductList'), Webpack uses this container to negotiate shared dependencies and download only the component code — not the entire remote bundle.
How do remote URLs change between local development and production?
In local development, remote URLs point to localhost with specific ports (e.g., Products@https://localhost:4002/remoteEntry.js). In production, they use relative paths served by a reverse proxy (e.g., Products@/products/remoteEntry.js). The publicPath in the remote's webpack config must match — https://localhost:4002/ locally vs /products/ in production.
What is the bootstrap pattern and why is it required?
The bootstrap pattern splits your entry point into two files: index.js calls import('./bootstrap'), and bootstrap.js contains the actual app code. This async boundary gives Module Federation time to negotiate shared dependencies before any shared module is used. Without it, you get the error "Shared module is not available for eager consumption."