React MFE Monorepo Setup

Setting Up a React Micro Frontend Monorepo with Turborepo

Setting up a React micro frontend monorepo with Turborepo is the foundation for building scalable, independently deployable frontend applications. In this step-by-step guide, you'll initialize a production-ready monorepo from scratch — with npm workspaces for package management, Turborepo (opens in a new tab) for build orchestration, and Webpack 5 Module Federation (opens in a new tab) for runtime integration.

By the end of this article, you'll have a working monorepo with a Host shell application, multiple Remote MFEs each running on their own port, and shared packages consumed across all MFEs.

This is a hands-on tutorial. Every code block comes from a real production monorepo with 11 React MFEs running on ports 4000–4011. We show both Local Development and Production configs — they are significantly different.

Prerequisites

Before starting, make sure you have:

  • Node.js 16+ installed (node -v)
  • npm 8+ (ships with Node.js — supports workspaces natively)
  • mkcert for local HTTPS certificates (optional but recommended for cross-origin MFE loading)

The Monorepo Structure

Here's the complete folder structure we're building. Study it before we start — every file has a purpose.

React Micro Frontend monorepo folder structure showing apps, packages, and configs directories with 11 MFE applications and shared packages

Complete monorepo structure
my-ecommerce-platform/
├── package.json              ← Root: npm workspaces + turbo scripts
├── turbo.json                ← Turborepo pipeline config
├── node_modules/             ← Single hoisted node_modules
│
├── apps/
│   ├── host/                 ← Host (Shell) — port 4000
│   │   ├── package.json
│   │   ├── webpack.config.js
│   │   ├── postcss.config.js
│   │   ├── tailwind.config.js
│   │   └── src/
│   │       ├── index.js      ← import('./bootstrap')
│   │       ├── bootstrap.js  ← React root + Provider + Router
│   │       ├── App.jsx       ← Routes + lazy-loaded MFEs
│   │       └── index.html
│   │
│   ├── onboarding/           ← Remote MFE — port 4001
│   │   ├── package.json
│   │   ├── webpack.config.js
│   │   └── src/
│   │       ├── index.js
│   │       └── OnboardingApp.jsx
│   │
│   ├── products/             ← Remote MFE — port 4002
│   ├── inventory/            ← Remote MFE — port 4003
│   ├── orders/               ← Remote MFE — port 4004
│   ├── pricing/              ← Remote MFE — port 4005
│   ├── earnings/             ← Remote MFE — port 4006
│   ├── analytics/            ← Remote MFE — port 4007
│   ├── reports/              ← Remote MFE — port 4008
│   ├── settings/             ← Remote MFE — port 4009
│   ├── support/              ← Remote MFE — port 4010
│   └── reviews/              ← Remote MFE — port 4011
│
├── packages/
│   └── core/
│       ├── store/            ← @myapp/store (shared Redux)
│       │   ├── package.json
│       │   ├── index.js
│       │   ├── store.js
│       │   ├── hooks.js
│       │   └── slices/
│       │       └── authSlice.js
│       └── api/              ← @myapp/api (shared axios)
│           ├── package.json
│           └── index.js
│
└── configs/                  ← Shared configs (optional)
    └── eslint-config/

Key insight: Each app under apps/ is a fully independent application with its own package.json, webpack.config.js, dev server, and build output. The packages/ directory contains shared code consumed by all MFEs via npm workspace linking.

Step 1 — Initialize the Monorepo Root

Start by creating the root package.json with npm workspaces. This tells npm to hoist shared dependencies into a single node_modules/ at the root.

package.json
{
  "name": "my-ecommerce-platform",
  "version": "1.0.0",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*",
    "packages/*/*",
    "configs/*"
  ],
  "scripts": {
    "dev": "turbo run dev --concurrency=16",
    "build": "turbo run build",
    "start": "turbo run start",
    "lint": "turbo run lint",
    "clean": "turbo run clean && rm -rf node_modules"
  },
  "devDependencies": {
    "autoprefixer": "^10.4.21",
    "postcss": "^8.5.6",
    "tailwindcss": "^3.4.17",
    "turbo": "^1.10.16"
  },
  "engines": {
    "node": ">=16.0.0"
  },
  "packageManager": "npm@10.x",
  "dependencies": {
    "axios": "^1.9.0"
  }
}

What each workspace pattern means:

PatternWhat It Matches
apps/*Each MFE app: apps/host, apps/products, apps/orders, etc.
packages/*Top-level packages
packages/*/*Nested packages: packages/core/store, packages/core/api
configs/*Shared configs: ESLint, Prettier, etc.

Run npm install at the root — npm resolves all workspace dependencies and hoists them into a single node_modules/. No duplicate React, no version conflicts.

Step 2 — Configure Turborepo

Turborepo (opens in a new tab) orchestrates builds across all workspace packages. The turbo.json file defines the build pipeline — which tasks depend on which, what outputs to cache, and which tasks run in parallel.

turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "start": {
      "dependsOn": ["build"],
      "cache": false
    }
  }
}

Pipeline breakdown:

TaskdependsOncachepersistentWhat It Does
build["^build"]yes (default)noBuild each app — but build dependencies first (^ means "build my deps first")
devnonefalsetrueStart dev servers — no caching (always fresh), persistent (keeps running)
start["build"]falsenoServe production builds — requires build to finish first

Now turbo run dev --concurrency=16 starts all 12 dev servers in parallel — Host on 4000, Onboarding on 4001, Products on 4002, all the way to Reviews on 4011.

Step 3 — Create the Host App (Shell)

The Host app is the shell application — it owns the layout (sidebar, header), routing, and loads Remote MFEs at runtime via Module Federation.

apps/host/package.json
{
  "name": "host-app",
  "version": "1.0.0",
  "description": "Shell application that hosts all micro-frontends",
  "main": "src/index.js",
  "scripts": {
    "dev": "webpack serve --config webpack.config.js",
    "build": "webpack build --config webpack.config.js",
    "start": "serve dist -p 4000"
  },
  "dependencies": {
    "@myapp/api": "0.0.1",
    "@myapp/store": "0.0.1",
    "@reduxjs/toolkit": "^2.6.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-redux": "^9.2.0",
    "react-router-dom": "^6.28.0"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@babel/preset-env": "^7.25.3",
    "@babel/preset-react": "^7.24.7",
    "autoprefixer": "^10.4.21",
    "babel-loader": "^9.1.3",
    "css-loader": "^7.1.2",
    "html-webpack-plugin": "^5.6.0",
    "mini-css-extract-plugin": "^2.9.0",
    "postcss": "^8.5.6",
    "postcss-loader": "^7.3.0",
    "serve": "^14.2.1",
    "tailwindcss": "^3.4.17",
    "webpack": "^5.93.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^5.0.4"
  }
}
⚠️

Critical: The Host app declares @myapp/api and @myapp/store as dependencies (not devDependencies). These are workspace-linked packages resolved from packages/core/api and packages/core/store. npm resolves them via the workspace protocol automatically.

Host Webpack Config (LOCAL vs PRODUCTION)

This is where local and production configs diverge significantly. Local uses HTTPS with localhost certificates, development mode, and localhost remote URLs. Production uses relative paths, production mode, and full optimization.

apps/host/webpack.config.js
// apps/host/webpack.config.js — LOCAL DEVELOPMENT
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { ModuleFederationPlugin } = require('webpack').container
const dependencies = require('./package.json').dependencies

// HTTPS for local dev (mkcert certificates)
const fs = require('fs')
const certPath = path.join(__dirname, '../../localhost.pem')
const keyPath = path.join(__dirname, '../../localhost-key.pem')

const httpsConfig = fs.existsSync(certPath) && fs.existsSync(keyPath)
  ? { type: 'https', options: { cert: fs.readFileSync(certPath), key: fs.readFileSync(keyPath) } }
  : { type: 'https', options: { requestCert: false, rejectUnauthorized: false } }

module.exports = {
  mode: 'development',
  entry: path.resolve(__dirname, 'src', 'index.js'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js',
    publicPath: '/',
    chunkFilename: '[name].bundle.js',
  },
  devServer: {
    server: httpsConfig,
    port: 4000,
    historyApiFallback: true,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
    },
  },
  resolve: {
    extensions: ['.js', '.jsx', '.json'],
    alias: {
      '@myapp/api':   path.resolve(__dirname, '../../packages/core/api'),
      '@myapp/store': path.resolve(__dirname, '../../packages/core/store'),
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'Host',
      filename: 'remoteEntry.js',
      remotes: {
        // LOCAL: Each MFE runs on its own localhost port
        Onboarding: 'Onboarding@https://localhost:4001/remoteEntry.js',
        Products:   'Products@https://localhost:4002/remoteEntry.js',
        Inventory:  'Inventory@https://localhost:4003/remoteEntry.js',
        Orders:     'Orders@https://localhost:4004/remoteEntry.js',
        Pricing:    'Pricing@https://localhost:4005/remoteEntry.js',
        Earnings:   'Earnings@https://localhost:4006/remoteEntry.js',
        Analytics:  'Analytics@https://localhost:4007/remoteEntry.js',
        Reports:    'Reports@https://localhost:4008/remoteEntry.js',
        Settings:   'Settings@https://localhost:4009/remoteEntry.js',
        Support:    'Support@https://localhost:4010/remoteEntry.js',
        Reviews:    'Reviews@https://localhost:4011/remoteEntry.js',
      },
      shared: {
        react:              { singleton: true, requiredVersion: dependencies.react },
        'react-dom':        { singleton: true, requiredVersion: dependencies['react-dom'] },
        'react-router-dom': { singleton: true, requiredVersion: dependencies['react-router-dom'] },
        '@reduxjs/toolkit':  { singleton: true, requiredVersion: '^2.6.0' },
        'react-redux':      { singleton: true, requiredVersion: '^9.2.0' },
        '@myapp/store':     { singleton: true, requiredVersion: '0.0.1' },
      },
    }),
    new MiniCssExtractPlugin(),
    new HtmlWebpackPlugin({ template: './src/index.html' }),
  ],
  module: {
    rules: [
      {
        test: /\.(jsx|js)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: { presets: ['@babel/preset-env', '@babel/preset-react'] },
        },
      },
      { test: /\.css$/i, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] },
      { test: /\.(png|jpeg|gif|jpg)$/i, type: 'asset/resource' },
    ],
  },
  optimization: { splitChunks: false },
}

Key differences between Local and Production:

ConfigLocal DevelopmentProduction / Server
mode'development''production'
HTTPSmkcert certificates loaded from ../../localhost.pemNot needed — Nginx handles SSL
Remote URLshttps://localhost:4001/remoteEntry.js/onboarding/remoteEntry.js
publicPath// (Host) or /products/ (Remote)
splitChunksDisabled (false)Enabled with vendor chunking
performanceNot configuredmaxEntrypointSize: 512000

Why HTTPS locally? Module Federation loads remote scripts cross-origin. Browsers block mixed content (HTTP resources on HTTPS pages). Using mkcert (opens in a new tab) to create trusted local certificates ensures all MFEs load without CORS errors.

Host Entry Point (Async Boundary)

Module Federation requires an async boundary before shared dependencies are used. The entry point does a dynamic import() which creates this boundary.

apps/host/src/index.js
// apps/host/src/index.js
// Async import for Module Federation — REQUIRED
import('./bootstrap')
apps/host/src/bootstrap.js
// apps/host/src/bootstrap.js
import React from 'react'
import { createRoot } from 'react-dom/client'
import { Provider, store } from '@myapp/store'
import { BrowserRouter } from 'react-router-dom'
import App from './App.jsx'
import './index.css'

const root = createRoot(document.getElementById('root'))

root.render(
  <BrowserRouter>
    <Provider store={store}>
      <App />
    </Provider>
  </BrowserRouter>
)

Why not render directly in index.js? Without the async boundary, Webpack cannot negotiate shared dependency versions before React loads. You'll get the error: Shared module is not available for eager consumption. The import('./bootstrap') pattern solves this for every Module Federation project.

Host App Component (Routes + Lazy MFEs)

The Host app loads each MFE lazily using React.lazy() with dynamic imports from Module Federation remotes. Each route maps to a remote MFE.

apps/host/src/App.jsx
// apps/host/src/App.jsx — Dynamic MFE loading with error boundaries
import React, { lazy, Suspense } from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import { useAppSelector, selectIsLoggedIn } from '@myapp/store'
import ErrorBoundary from './components/ErrorBoundary'
import Sidebar from './components/Sidebar'
import Loading from './components/Loading'

// Lazy-load each MFE — loaded at runtime via Module Federation
const Onboarding = lazy(() => import('Onboarding/OnboardingApp'))
const Products   = lazy(() => import('Products/ProductCatalog'))
const Inventory  = lazy(() => import('Inventory/InventoryApp'))
const Orders     = lazy(() => import('Orders/OrdersApp'))
const Pricing    = lazy(() => import('Pricing/PricingApp'))
const Earnings   = lazy(() => import('Earnings/EarningsApp'))
const Analytics  = lazy(() => import('Analytics/AnalyticsApp'))
const Settings   = lazy(() => import('Settings/SettingsApp'))
const Support    = lazy(() => import('Support/SupportApp'))

function ProtectedRoute({ children }) {
  const isLoggedIn = useAppSelector(selectIsLoggedIn)
  if (!isLoggedIn) return <Navigate to="/login" replace />
  return children
}

export default function App() {
  return (
    <div className="flex h-screen">
      <Sidebar />
      <main className="flex-1 overflow-auto bg-gray-50">
        <ErrorBoundary>
          <Suspense fallback={<Loading />}>
            <Routes>
              <Route path="/onboarding/*" element={<Onboarding />} />
              <Route path="/products/*" element={
                <ProtectedRoute><Products /></ProtectedRoute>
              } />
              <Route path="/inventory/*" element={
                <ProtectedRoute><Inventory /></ProtectedRoute>
              } />
              <Route path="/orders/*" element={
                <ProtectedRoute><Orders /></ProtectedRoute>
              } />
              <Route path="/pricing/*" element={
                <ProtectedRoute><Pricing /></ProtectedRoute>
              } />
              <Route path="/earnings/*" element={
                <ProtectedRoute><Earnings /></ProtectedRoute>
              } />
              <Route path="/analytics/*" element={
                <ProtectedRoute><Analytics /></ProtectedRoute>
              } />
              <Route path="/settings/*" element={
                <ProtectedRoute><Settings /></ProtectedRoute>
              } />
              <Route path="/support/*" element={
                <ProtectedRoute><Support /></ProtectedRoute>
              } />
            </Routes>
          </Suspense>
        </ErrorBoundary>
      </main>
    </div>
  )
}

Step 4 — Create a Remote MFE

Each Remote MFE is a standalone React application that exposes components via remoteEntry.js. The Host loads these at runtime.

apps/products/package.json
{
  "name": "products-mfe",
  "version": "1.0.0",
  "description": "Products micro-frontend",
  "main": "src/index.js",
  "scripts": {
    "dev": "webpack serve --config webpack.config.js",
    "build": "webpack build --config webpack.config.js",
    "start": "serve dist -p 4002"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router-dom": "^6.28.0",
    "react-icons": "^5.5.0"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@babel/preset-env": "^7.25.3",
    "@babel/preset-react": "^7.24.7",
    "autoprefixer": "^10.4.21",
    "babel-loader": "^9.1.3",
    "css-loader": "^7.1.2",
    "html-webpack-plugin": "^5.6.0",
    "postcss": "^8.5.3",
    "postcss-loader": "^8.1.1",
    "serve": "^14.2.1",
    "tailwindcss": "^3.4.17",
    "webpack": "^5.93.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^5.0.4"
  }
}

Remote Webpack Config (LOCAL vs PRODUCTION)

apps/products/webpack.config.js
// apps/products/webpack.config.js — LOCAL DEVELOPMENT
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { ModuleFederationPlugin } = require('webpack').container
const dependencies = require('./package.json').dependencies

// HTTPS for local dev
const fs = require('fs')
const certPath = path.join(__dirname, '../../localhost.pem')
const keyPath = path.join(__dirname, '../../localhost-key.pem')
const httpsConfig = fs.existsSync(certPath) && fs.existsSync(keyPath)
  ? { type: 'https', options: { cert: fs.readFileSync(certPath), key: fs.readFileSync(keyPath) } }
  : { type: 'https', options: { requestCert: false, rejectUnauthorized: false } }

module.exports = {
  mode: 'development',
  entry: path.resolve(__dirname, 'src', 'index.js'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js',
    // LOCAL: publicPath must match the localhost port
    publicPath: 'https://localhost:4002/',
    clean: true,
  },
  devServer: {
    server: httpsConfig,
    port: 4002,
    historyApiFallback: true,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'Products',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductCatalog':   './src/components/ProductCatalog.jsx',
        './ProductImages':    './src/components/ProductImages.jsx',
        './BulkUploadPage':   './src/components/BulkUploadPage.jsx',
        './SingleUploadPage': './src/components/SingleUploadPage.jsx',
      },
      shared: {
        react:              { singleton: true, requiredVersion: dependencies.react },
        'react-dom':        { singleton: true, requiredVersion: dependencies['react-dom'] },
        'react-router-dom': { singleton: true, requiredVersion: dependencies['react-router-dom'] },
        '@myapp/store':     { singleton: true, requiredVersion: '0.0.1' },
      },
    }),
    new MiniCssExtractPlugin(),
    new HtmlWebpackPlugin({ template: './src/index.html' }),
  ],
  module: {
    rules: [
      {
        test: /\.(jsx|js)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: { presets: ['@babel/preset-env', '@babel/preset-react'] },
        },
      },
      { test: /\.css$/i, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] },
      { test: /\.(png|jpeg|gif|jpg)$/i, type: 'asset/resource' },
    ],
  },
  optimization: { splitChunks: false },
}

Key differences for Remote MFEs:

ConfigLocal DevelopmentProduction / Server
publicPath'https://localhost:4002/''/products/'
HTTPSmkcert certificatesNot needed
splitChunksDisabledVendor chunking enabled
optimizationNonemoduleIds: 'deterministic'
⚠️

publicPath must match the URL where this MFE is served. Locally, that's https://localhost:4002/. On the server, Nginx serves it at /products/. If publicPath is wrong, chunk loading fails with 404 errors.

Step 5 — Create Shared Packages

Shared packages are consumed by all MFEs via workspace linking. They're declared in packages/ and imported like normal npm packages.

@myapp/store — Shared Redux Store

packages/core/store/package.json
// packages/core/store/package.json
{
  "name": "@myapp/store",
  "version": "0.0.1",
  "private": true,
  "main": "index.js",
  "dependencies": {
    "@reduxjs/toolkit": "^2.6.0",
    "react-redux": "^9.2.0"
  }
}
packages/core/store/index.js
// packages/core/store/index.js
// Re-export everything MFEs need from the shared store
export { store } from './store'
export { Provider } from 'react-redux'
export { useAppDispatch, useAppSelector } from './hooks'

// Auth slice exports
export {
  setIsLoggedIn,
  setUserInfo,
  setAuthToken,
  setSessionExpired,
  selectAuthToken,
  selectUser,
  selectIsLoggedIn,
  selectPermissions,
  selectSessionExpired,
} from './slices/authSlice'

@myapp/api — Shared API Layer

packages/core/api/package.json
// packages/core/api/package.json
{
  "name": "@myapp/api",
  "version": "0.0.1",
  "private": true,
  "main": "index.js"
}

Both packages are resolved by the alias config in Webpack and shared as singletons via Module Federation — ensuring all MFEs use the exact same Redux store instance and axios interceptors. Read more in MFE Communication Patterns.

Step 6 — Tailwind CSS Configuration

Each MFE needs its own postcss.config.js and tailwind.config.js. The configs are identical in structure — only the content paths change per app.

apps/host/postcss.config.js
// apps/host/postcss.config.js (same for ALL MFE apps)
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
apps/host/tailwind.config.js
// apps/host/tailwind.config.js (each MFE has its own)
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './src/**/*.{js,jsx,ts,tsx}',
    './src/index.html',
    './public/index.html',
    './index.html',
  ],
  theme: { extend: {} },
  plugins: [],
}

Running All MFEs Together

With everything configured, start the entire monorepo with one command:

npm run dev

This runs turbo run dev --concurrency=16, which starts all 12 dev servers in parallel:

Turborepo running 12 MFE dev servers in parallel on ports 4000 to 4011 with turbo run dev command output

AppPortURL
Host (Shell)4000https://localhost:4000
Onboarding4001https://localhost:4001
Products4002https://localhost:4002
Inventory4003https://localhost:4003
Orders4004https://localhost:4004
Pricing4005https://localhost:4005
Earnings4006https://localhost:4006
Analytics4007https://localhost:4007
Reports4008https://localhost:4008
Settings4009https://localhost:4009
Support4010https://localhost:4010
Reviews4011https://localhost:4011

Open https://localhost:4000 — the Host app loads, and as you navigate to /products or /orders, Module Federation dynamically loads the corresponding MFE's remoteEntry.js over the network.

You don't need to run all 12. If you're only working on the Products MFE, run just the Host and Products: turbo run dev --filter=host-app --filter=products-mfe. Other MFE routes will show the error boundary fallback until those servers are started.

Common Mistakes and Fixes

ProblemCauseFix
Shared module is not available for eager consumptionMissing async boundaryUse import('./bootstrap') in entry point
Remote MFE returns 404 for chunksWrong publicPathMatch publicPath to the URL where MFE is served
Duplicate React instancessingleton: true missingAdd singleton: true to all shared dependencies
CORS errors loading remoteMissing CORS headersAdd Access-Control-Allow-Origin: * in devServer headers
Styles not loading in MFEMissing PostCSS/Tailwind configEach MFE needs its own postcss.config.js + tailwind.config.js
Cannot find module '@myapp/store'Missing webpack aliasAdd alias in resolve pointing to ../../packages/core/store

What's Next?

You now have a working React Micro Frontend monorepo with Turborepo. In the next article, we'll dive deep into Creating Shared Packages — building the @myapp/store (Redux), @myapp/api (axios + interceptors), and @myapp/uicomponents (shared UI) packages that all MFEs consume.

React Micro Frontend monorepo architecture diagram showing Host shell loading 11 remote MFEs through Module Federation with shared packages at the foundation

← Back to Micro Frontend vs SPA

Continue to Shared Packages in MFE Monorepo →


Frequently Asked Questions

What is the best way to set up a React Micro Frontend monorepo?

The best approach is to use npm workspaces for package management, Turborepo for build orchestration, and Webpack 5 Module Federation for runtime integration. This combination gives you a single node_modules, parallel builds, and independent MFE deployments.

Why use Turborepo for Micro Frontends?

Turborepo provides parallel task execution, intelligent caching, and dependency-aware build ordering. In a 12-MFE monorepo, running turbo run dev --concurrency=16 starts all MFE dev servers in parallel. Building only rebuilds what changed. This cuts build time from minutes to seconds.

What is the difference between local and production webpack config in MFE?

In local development, remotes point to https://localhost:PORT/remoteEntry.js, mode is development, HTTPS certificates are loaded for local SSL, and splitChunks is disabled. In production, remotes point to relative paths like /products/remoteEntry.js, mode is production, no HTTPS config is needed, and splitChunks with vendor chunking is enabled for optimal bundle sizes.

Why does the MFE entry point use import('./bootstrap') instead of direct rendering?

Module Federation requires an async boundary before using shared dependencies. The dynamic import('./bootstrap') creates this boundary, allowing Webpack to negotiate shared dependency versions (React, Redux) before any code executes. Without it, you get Shared module is not available for eager consumption errors.

How many Micro Frontends can you have in one monorepo?

There is no hard limit. Production systems commonly run 8 to 15 MFEs in a single monorepo. The example in this article uses 11 remote MFEs plus a Host shell, each on its own port (4000 to 4011). Turborepo handles the build orchestration with --concurrency=16 to run all dev servers in parallel.

Can I use Yarn or pnpm instead of npm workspaces?

Yes. Turborepo works with npm workspaces, Yarn workspaces, and pnpm workspaces. The Module Federation configuration stays the same regardless of your package manager. npm workspaces is used here because it requires zero additional tooling — it ships with Node.js.