DEV Community

Jakub Zagórski
Jakub Zagórski

Posted on

Guide to modern microfrontends with Module Federation 2.0

The Ultimate Guide: Bootstrapping Microfrontends with Rsbuild, React, and Module Federation 2.0

Have you ever stared at a massive, monolithic frontend codebase and thought, "There has to be a better way"? Welcome to the world of Microfrontends! By decoupling your massive frontend into smaller, independently developed, and deployed applications, you unlock faster build times, independent team workflows, and a more scalable architecture.

However, setting up the foundation for microfrontends from scratch can feel like navigating a minefield of configuration files and dependency conflicts. Not anymore!

In this comprehensive guide, we are going to build a blazing-fast, modern microfrontend architecture using the ultimate trifecta:

  • React 19
  • React Router 7
  • Rsbuild 1.x.x combined with Module Federation 2.0

By the end of this tutorial, you will have bootstrapped a working Provider (Remote) app that exposes components, and a Shell (Host) app that gracefully consumes them.

Note: This tutorial assumes you possess basic terminal knowledge, are running Node.js (v22 or higher), and use npm.


🏗️ Step 1: Setting Up the Workspace

Let's create a directory to hold both of our applications:

mkdir my-mfe-workspace
cd my-mfe-workspace

# Create the two application directories
mkdir app1 shell
Enter fullscreen mode Exit fullscreen mode

📦 Step 2: Bootstrapping the Provider (Remote) App (app1)

The Provider app (often called a Remote) will expose a React component that the Shell app can consume.

2.1 Initialize and Install Dependencies

Navigate to your app1 directory:

cd app1
npm init -y
Enter fullscreen mode Exit fullscreen mode

Now, let's install the exact packages you need. We will install React 19 and React Router 7 as dependencies, and the required Rsbuild tools as devDependencies.

# Core Dependencies
npm install react@19 react-dom@19 react-router-dom@7

# Dev Dependencies (Build Tools & Types)
npm install -D @rsbuild/core@1.7.0 @rsbuild/plugin-react@1.4.0 @module-federation/rsbuild-plugin@2.1.0 typescript @types/react @types/react-dom
Enter fullscreen mode Exit fullscreen mode

2.2 Configure Rsbuild

Create an rsbuild.config.ts file in the root of app1:

// app1/rsbuild.config.ts
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';

export default defineConfig(({ env }) => ({
  plugins: [
    pluginReact(),
    pluginModuleFederation({
      name: 'app1',
      exposes: { 
        './App': './src/federation.tsx' 
      },
      dts: false,
      dev: {
        disableLiveReload: false,
      },
      shared: {
        react: { singleton: true, requiredVersion: '^19.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
        'react-router-dom': { singleton: true, requiredVersion: '^7.0.0' },
      },
    }),
  ],
  server: {
    port: 3002,
    cors: true,
  },
  output: {
    assetPrefix: env === 'production' ? '/my-remote-app1/' : '/',
  },
}));
Enter fullscreen mode Exit fullscreen mode

2.3 Create the Source Files

We need a few files to make this a working React application. First, let's create the required directory structure:

mkdir src
touch index.html src/main.tsx src/bootstrap.tsx src/App.tsx src/federation.tsx tsconfig.json
Enter fullscreen mode Exit fullscreen mode

tsconfig.json (Base Typescript Config):

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
Enter fullscreen mode Exit fullscreen mode

index.html (The HTML Template):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>App 1 (Remote)</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

src/App.tsx (The Main Component):

import React from 'react';

const App = () => {
  return (
    <div style={{ border: '2px dashed blue', padding: '20px', margin: '20px' }}>
      <h2>Hello from App 1 (Remote)</h2>
      <p>This is a microfrontend exposed via Module Federation 2.0!</p>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

src/federation.tsx (The Exposed Entry Point):
Note: We highly recommend exposing a file separated from the main root render logic to ensure clean imports for the Shell app.

export { default } from './App';
Enter fullscreen mode Exit fullscreen mode

src/bootstrap.tsx (The App Initializer):
Module Federation requires an async boundary to load shared dependencies. bootstrap.tsx provides this boundary.

import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

const container = document.getElementById('root');
if (container) {
  const root = createRoot(container);
  root.render(
    <React.StrictMode>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </React.StrictMode>
  );
}
Enter fullscreen mode Exit fullscreen mode

src/main.tsx (The Async Entry):

import('./bootstrap');
Enter fullscreen mode Exit fullscreen mode

Add these scripts to your app1/package.json:

"scripts": {
  "start": "rsbuild dev",
  "build": "rsbuild build"
}
Enter fullscreen mode Exit fullscreen mode

🚢 Step 3: Bootstrapping the Shell (Host) App

The Shell application orchestrates navigation and dynamically imports the Remote component.

3.1 Initialize and Install Dependencies

Navigate to your shell directory:

cd ../shell
npm init -y
Enter fullscreen mode Exit fullscreen mode

Install the exact same dependencies as the Provider app to ensure our shared constraints (^19.0.0, ^7.0.0) match perfectly:

npm install react@19 react-dom@19 react-router-dom@7
npm install -D @rsbuild/core@1.7.0 @rsbuild/plugin-react@1.4.0 @module-federation/rsbuild-plugin@2.1.0 typescript @types/react @types/react-dom
Enter fullscreen mode Exit fullscreen mode

3.2 Configure Rsbuild

Create rsbuild.config.ts in the root of shell:

// shell/rsbuild.config.ts
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { pluginModuleFederation } from "@module-federation/rsbuild-plugin";

const createMfUrl = (name: string, env: string, port: number) =>
  env === "production"
    ? `/my-remote-${name}/mf-manifest.json`
    : `http://localhost:${port}/mf-manifest.json`;

const sharedPackageConfig = {
  singleton: true,
  eager: true,
};

export default defineConfig(({ env }) => ({
  plugins: [
    pluginReact(),
    pluginModuleFederation({
      name: "shell",
      dts: false,
      shareStrategy: "loaded-first",
      remotes: {
        app1: `app1@${createMfUrl("app1", env, 3002)}`,
      },
      shared: {
        react: { ...sharedPackageConfig, requiredVersion: "^19.0.0" },
        "react-dom": { ...sharedPackageConfig, requiredVersion: "^19.0.0" },
        "react-router-dom": { ...sharedPackageConfig, requiredVersion: "^7.0.0" },
      },
    }),
  ],
  server: {
    port: 3000,
    historyApiFallback: true, // Crucial for React Router client-side routing
  },
}));
Enter fullscreen mode Exit fullscreen mode

3.3 Create the Source Files

Like the provider app, set up the layout:

mkdir src
touch index.html src/main.tsx src/bootstrap.tsx src/App.tsx tsconfig.json src/global.d.ts
Enter fullscreen mode Exit fullscreen mode

Copy the same tsconfig.json and index.html from app1 into the shell folder, but optionally update the title in index.html to <title>Microfrontend Shell</title>.

src/global.d.ts (Silence Typescript errors for remote modules):

declare module 'app1/App' {
  const App: React.ComponentType;
  export default App;
}
Enter fullscreen mode Exit fullscreen mode

src/App.tsx ( The Host Routing Logic ):

import React, { lazy, Suspense } from 'react';
import { Routes, Route, Link } from 'react-router-dom';

// Dynamically import the remote!
const RemoteApp1 = lazy(() => import('app1/App'));

const ShellLayout = () => {
  return (
    <div style={{ fontFamily: 'system-ui, sans-serif' }}>
      <nav style={{ padding: '20px', background: '#333', color: 'white' }}>
        <ul style={{ display: 'flex', gap: '20px', listStyle: 'none', margin: 0, padding: 0 }}>
          <li><Link to="/" style={{ color: 'white', textDecoration: 'none' }}>Home</Link></li>
          <li><Link to="/app1" style={{ color: 'white', textDecoration: 'none' }}>App 1</Link></li>
        </ul>
      </nav>

      <main style={{ padding: '20px' }}>
        <Suspense fallback={<h2>Loading Microfrontend...</h2>}>
          <Routes>
            <Route path="/" element={<h1>Welcome to the Microfrontend Shell</h1>} />
            <Route path="/app1/*" element={<RemoteApp1 />} />
            <Route path="*" element={<h2>404 - Not Found</h2>} />
          </Routes>
        </Suspense>
      </main>
    </div>
  );
};

export default ShellLayout;
Enter fullscreen mode Exit fullscreen mode

src/bootstrap.tsx (The Shell Initializer):

import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

const container = document.getElementById('root');
if (container) {
  const root = createRoot(container);
  root.render(
    <React.StrictMode>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </React.StrictMode>
  );
}
Enter fullscreen mode Exit fullscreen mode

src/main.tsx (The Async Entry):

import('./bootstrap');
Enter fullscreen mode Exit fullscreen mode

Add the scripts to your shell/package.json:

"scripts": {
  "start": "rsbuild dev",
  "build": "rsbuild build"
}
Enter fullscreen mode Exit fullscreen mode

🚀 Step 4: Run It!

You are now ready to test your setup. Open two separate terminal windows.

Terminal 1 (Provider/App1):

cd app1
npm start
Enter fullscreen mode Exit fullscreen mode

Terminal 2 (Host/Shell):

cd shell
npm start
Enter fullscreen mode Exit fullscreen mode

Navigate your browser to http://localhost:3000. You will see the Shell navigation bar. Click on App 1, and you will see your remotely loaded Module Federation application rendered seamlessly via React Router!

The "Async Margin" (Why main.tsx and bootstrap.tsx?)

You'll notice we split the entry into main.tsx doing a dynamic import('./bootstrap') in both applications. This is mandatory in Webpack/Rsbuild Module Federation.

Because react and react-dom are defined as shared dependencies, Rsbuild needs an asynchronous rendering boundary to pause script execution, look up the module federation manifests, and download the shared react packages before executing the React application code. If you bypass this, you will encounter immediate errors complaining that react was not loaded before usage!

Top comments (0)