Module Federation Host and Remote Apps
You understand how Module Federation works — the plugin, shared dependencies, and remoteEntry.js. But knowing the configuration is different from knowing how to architect the host and remote apps themselves. How should the host app load remotes? What happens when a remote fails? How do remotes run standalone during development? How do you guard routes with permissions?
This article covers the practical architecture of module federation host and remote apps — host app structure, remote app structure, error handling with React.lazy() and error boundaries, standalone remote development, permission-based routing, bi-directional federation, and the differences between React and Next.js host setups.
This is Article 13 in the Micro Frontend Architecture series. If you haven't set up Module Federation yet, start with Webpack Module Federation — Complete Guide.

What Does Each App Do?
Before diving into code, understand the clear separation of responsibilities between the module federation host app and each remote app.
| Responsibility | Host App | Remote App |
|---|---|---|
| Routing | Owns top-level routes | Handles sub-routes within its domain |
| Layout | Provides shell (header, sidebar, nav) | Renders content area only |
| State store | Creates and provides Redux store | Accesses store via shared package |
| Authentication | Manages auth state and tokens | Reads auth state from shared store |
| Error handling | ErrorBoundary wraps each remote | Handles internal component errors |
| Loading states | Suspense fallback for lazy loading | N/A (host controls this) |
| Shared dependencies | Loads React, Router, Redux first | Uses host's shared instances |
| Build & deploy | Deploys as main entry point | Deploys independently to its own path |
| Dev server | Runs on port 4000 (main port) | Runs on port 4001-4011 |
publicPath | / | /mfe-name/ (e.g., /products/) |
The host owns the shell — everything the user sees regardless of which page they are on (header, sidebar, navigation, footer). The remote owns the content — the actual feature UI that renders inside the host's content area. This separation mirrors the micro frontends concept described by Martin Fowler (opens in a new tab) — each team owns a vertical slice of the product. The host team never touches product code, and the products team never touches navigation.
The host is the orchestrator, not the owner. The host does not know what the Products component renders internally. It only knows that Products/ProductList exists and where to find it. This is the core principle of micro frontend architecture — independent team ownership.
Host App Structure
The host app is the main entry point. It contains the layout shell, routing configuration, Redux store provider, and React.lazy() imports for all remote modules.
The Bootstrap Entry Point
Every Module Federation app — host or remote — needs the async bootstrap pattern. The host's bootstrap wraps the entire app with BrowserRouter, Redux Provider, and the shared store.
The import("./bootstrap") in index.js creates the async boundary. Webpack uses this gap to load all remoteEntry.js files, negotiate shared dependency versions, and resolve singletons — all before bootstrap.js executes. Without this pattern, you get the "Shared module is not available for eager consumption" error.
Host Webpack Configuration
The host's webpack.config.js defines the remotes object — a map of every MFE the host can consume. The URLs are completely different between local development and production.
The host does NOT need filename or exposes. The host only consumes remotes — it does not expose anything. Only remote apps need filename: "remoteEntry.js" and exposes. If the host also needs to expose modules (bi-directional federation), see the bi-directional section below.
Loading Remotes with React.lazy() and Error Handling
The host uses React.lazy() to dynamically import remote modules. Each import includes a .catch() handler that returns a fallback component when the remote is unavailable — this prevents the entire host from crashing if one MFE is down.
The .catch() on each import() is critical. Without it, a single remote failing to load crashes the entire host application. With .catch(), the failed remote renders a fallback message while every other route continues working normally. This pattern follows the React documentation on code splitting (opens in a new tab) — each lazy-loaded boundary is an independent failure domain.
Remote App Structure
Each remote app is a fully independent React application that can run on its own. It exposes specific components to the host via Module Federation's exposes config, but it also has its own App.jsx, routing, and HTML template for standalone development.
Remote Webpack Configuration
The remote's webpack.config.js uses the same ModuleFederationPlugin but with exposes instead of remotes. The publicPath is the most critical difference between local and production — it must match the URL the host uses to fetch chunks.
Standalone Remote Development
Every remote needs its own bootstrap.js and App.jsx for standalone development. This lets developers work on the Products MFE without running the host or any other MFE — the remote renders its own UI on https://localhost:4002.
The standalone App.jsx is never loaded by the host. When the host imports Products/ProductList, it only gets the ProductList component from exposes — the remote's App.jsx, bootstrap.js, and routing are completely ignored. The standalone setup exists purely for independent development and testing.
Error Handling — ErrorBoundary for Remote Components
The .catch() on React.lazy() handles load failures (network errors, remote not running). But what about render failures — when the remote loads successfully but throws an error during rendering? For this, you need a React Error Boundary.
Combining ErrorBoundary with Suspense
The recommended pattern wraps each remote in both ErrorBoundary (for render errors) and Suspense (for loading states):
The loading order is: ErrorBoundary → Suspense → RemoteComponent. If the import fails, .catch() provides a fallback. If the import succeeds but rendering fails, ErrorBoundary catches the error. If the import is in progress, Suspense shows the loading shimmer.
Permission-Based Remote Loading
In real-world applications, not every user should access every MFE. The host can guard remote routes with a permission check that reads from the shared Redux store.
The PermissionRoute component reads the user's permissions from the shared Redux store and either renders the remote or shows an access denied message. The remote MFE itself does not need to know about permissions — the host handles it before the remote even loads.

Bi-Directional Federation
A Module Federation app can be both a host and a remote at the same time. This is useful when an MFE needs to consume components from other remotes while also exposing its own components.
In this example, Dashboard exposes AnalyticsWidget and RevenueChart (making it a remote), while also consuming Products/ProductList and Orders/OrdersDashboard (making it a host). Any other app can import("Dashboard/AnalyticsWidget") to embed the analytics widget in their own UI.
Avoid circular dependencies. If App A consumes App B and App B consumes App A, you create a circular remote dependency. Webpack can handle this at the config level, but it creates complex load ordering issues. If two apps need each other's components, extract the shared components into a third "shared components" remote instead.
React Host vs Next.js Host
If your host app uses Next.js instead of React, the remote loading pattern changes. Next.js uses next/dynamic with ssr: false instead of React.lazy(), and the plugin is NextFederationPlugin instead of ModuleFederationPlugin.
Key Differences
| Feature | React Host (Webpack) | Next.js Host (NextFederationPlugin) |
|---|---|---|
| Plugin | ModuleFederationPlugin | NextFederationPlugin |
| Config file | webpack.config.js | next.config.js |
| Loading remotes | React.lazy() + Suspense | next/dynamic with ssr: false |
| Error handling | .catch() on import() | .catch() on import() |
| SSR support | No (client-side only) | Yes (with isServer check) |
| Remote entry path (React) | /mfe-name/remoteEntry.js | /mfe-name/remoteEntry.js |
| Remote entry path (Next) | N/A | /mfe/_next/static/chunks/remoteEntry.js |
| Shared deps config | shared: { react: ... } | shared: { react: ... } |
| Extra options | None | exposePages, enableImageLoaderFix |
| Bootstrap pattern | Required (index.js → bootstrap.js) | Not needed (Next.js handles it) |
The most important difference: Next.js remotes serve remoteEntry.js at _next/static/chunks/remoteEntry.js (not at the root path like React remotes). The Next.js host config uses an isServer check to load ssr/remoteEntry.js during server rendering and chunks/remoteEntry.js in the browser. This is covered in detail in a dedicated Next.js Module Federation article later in this series.
Development Workflow
Running a Module Federation setup locally requires starting the host and whichever remotes you are working on. You do not need to run all remotes — the host's .catch() fallback handles missing ones gracefully.
Production Deployment
In production, each MFE builds independently and deploys to its own URL path. A reverse proxy (Nginx, Kubernetes Ingress, or CDN) routes requests to the correct MFE's static assets based on the URL path.

The key rule: each remote's publicPath (opens in a new tab) must exactly match the URL path that the reverse proxy maps to its assets. If the reverse proxy serves Products at /products/, the Products webpack config must have publicPath: "/products/". A mismatch causes chunk loading errors — remoteEntry.js loads but subsequent chunk requests go to the wrong path.
What's Next?
← Back to Webpack Module Federation — Complete Guide
Continue to Shared Dependencies & Singleton Pattern →
Frequently Asked Questions
What is the difference between a host and remote in Module Federation?
The host is the main application shell that defines which remotes to consume via the remotes config. It owns top-level routing, layout, authentication, and the Redux store. The remote is an independently built app that exposes specific components via the exposes config. The host loads these components at runtime using React.lazy() or next/dynamic. Each remote can also run standalone for independent development.
Can an app be both a host and a remote at the same time?
Yes — this is called bi-directional federation. An app can define both remotes (to consume other apps) and exposes with a filename (to let other apps consume its modules). For example, a Dashboard app can expose analytics widgets while consuming product and order components from other MFEs.
How do you handle errors when a remote app fails to load?
Use two layers of error handling. First, add a .catch() callback to the dynamic import inside React.lazy() — this handles network failures and returns a fallback component. Second, wrap each remote with a React ErrorBoundary component — this catches runtime rendering errors after the module loads successfully. Together, they ensure the host app never crashes when a remote is unavailable.
Why does the remote need its own bootstrap.js for standalone development?
The bootstrap.js file in a remote app serves two purposes. First, it creates the async boundary that Module Federation requires to negotiate shared dependencies before app code runs. Second, it provides a standalone render setup (ReactDOM.createRoot, BrowserRouter, Redux Provider) so developers can run and test the remote independently on its own port without needing the host running.
What is the difference between React.lazy() and next/dynamic for loading remote modules?
React.lazy() is used in React host apps with Webpack's ModuleFederationPlugin. It requires a Suspense wrapper for loading states. next/dynamic is used in Next.js host apps with NextFederationPlugin. It must include ssr: false to prevent server-side rendering of remote components (since remote JS only exists in the browser). Both support .catch() for error handling, but next/dynamic has a built-in loading option instead of requiring a separate Suspense component.
How does publicPath differ between host and remote in production?
The host app typically uses publicPath: '/' since it serves as the main entry point at the root domain. Each remote uses publicPath: '/mfe-name/' (e.g., /products/, /orders/) matching the reverse proxy route that serves its static assets. In local development, the host uses publicPath: '/' while remotes use publicPath: 'https://localhost:PORT/' with their assigned dev server port.