Next.js MFE Host App Setup

Published: April 19, 2026 · 14 min read

Next.js Micro Frontend Host App: Complete Setup Guide

You have multiple micro frontend teams. Some build with React, others with Next.js. You need a single Next.js micro frontend host app that loads all these remote MFEs at runtime — shares a Redux store across every module, handles SSR for the Next.js remotes, and wraps everything with PWA support, image optimization, and Content Security Policy headers. In the previous article, you learned the differences between NextFederationPlugin and ModuleFederationPlugin. Now it is time to build the host application that puts it all together.

In this guide, you will:

  • Set up a Next.js host with NextFederationPlugin and define both React and Next.js remotes
  • Configure shared singleton dependencies that every remote reuses
  • Wire up _app.tsx with a shared Redux Provider
  • Consume remote components with next/dynamic and handle loading states
  • Add image optimization, PWA, bundle analysis, and security headers
  • See the complete production-ready next.config.js from a real-world project

Next.js micro frontend host app architecture diagram showing the host loading React and Next.js remote MFEs at runtime

What Is a Next.js Micro Frontend Host App?

The host app is the container application — the shell that users navigate to. It owns the top-level layout (header, footer, sidebar), the shared Redux store, and the routing system. Every other micro frontend (Auth, Products, Cart, Content, Support) is a remote — an independent application that the host loads at runtime via Webpack Module Federation (opens in a new tab).

In a React-only architecture, the host is a React SPA using ModuleFederationPlugin. When the host is Next.js, it uses NextFederationPlugin instead — gaining SSR support, built-in image optimization, file-based routing, and security headers that React SPAs handle manually.

A Next.js host can load both React remotes and Next.js remotes simultaneously. React remotes (Auth, Cart, Account, Support) use plain remoteEntry.js paths. Next.js remotes (Products, Content) use _next/static/chunks/remoteEntry.js with the isServer toggle for SSR.

Prerequisites

Before starting, ensure you have:

Step 1 — Install Dependencies

The host application needs Next.js, the Module Federation plugin, shared monorepo packages, and development tools.

apps/Main/package.json
{
  "name": "main",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next dev -p 5000",
    "build": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next build",
    "start": "cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next start -p 5000",
    "analyze": "cross-env ANALYZE=true npm run build"
  },
  "dependencies": {
    "@myapp/api": "1.0.0",
    "@myapp/seo": "1.0.0",
    "@myapp/store": "1.0.0",
    "@module-federation/nextjs-mf": "^8.1.0",
    "@reduxjs/toolkit": "^2.0.1",
    "axios": "^1.13.1",
    "next": "^14.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-redux": "^9.0.4",
    "webpack": "^5.89.0"
  },
  "devDependencies": {
    "@next/bundle-analyzer": "^16.0.3",
    "cross-env": "^10.1.0",
    "eslint-config-next": "^14.0.0",
    "typescript": "^5.0.0"
  }
}

Key dependencies to note:

  • @module-federation/nextjs-mf (opens in a new tab) — the NextFederationPlugin package
  • webpack: ^5.89.0 — explicit webpack 5 dependency (required when using NEXT_PRIVATE_LOCAL_WEBPACK=true)
  • @myapp/store, @myapp/api, @myapp/seo — shared monorepo packages consumed as singleton dependencies
  • cross-env — sets NEXT_PRIVATE_LOCAL_WEBPACK=true across platforms
⚠️

NEXT_PRIVATE_LOCAL_WEBPACK=true is required in every npm script. Without it, Next.js uses its internal bundled webpack which does not expose the container plugin API. Module Federation silently fails — no errors in the console, but remotes never load. This is the number one "it builds but remotes are empty" issue.

Step 2 — Initialize NextFederationPlugin

Create next.config.js in the host app root. The minimum viable configuration needs three things: the plugin name, the filename for the remote entry, and the remotes map.

const { NextFederationPlugin } = require('@module-federation/nextjs-mf');
 
module.exports = {
  output: 'standalone',
  webpack(config, { isServer }) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'Main',
        filename: 'static/chunks/remoteEntry.js',
        remotes: { /* defined in Step 3 */ },
        shared: { /* defined in Step 4 */ },
        extraOptions: { /* defined in Step 5 */ },
      })
    );
    return config;
  },
};
  • name: 'Main' — unique identifier for this container in the Module Federation scope
  • filename: 'static/chunks/remoteEntry.js' — follows the Next.js asset convention (all chunks live under _next/static/chunks/)
  • output: 'standalone' — generates a self-contained build folder for Docker deployment

Step 3 — Define Remote Micro Frontends

The remotes object maps each remote MFE name to its remoteEntry.js URL. The URL format differs between React remotes and Next.js remotes, and between production and local development.

apps/Main/next.config.js
// apps/Main/next.config.js — Production / Server remotes
remotes: {
  // React remotes — plain remoteEntry.js at root path
  // These MFEs are built with ModuleFederationPlugin (webpack)
  // Nginx serves their static files from /auth/, /account/, etc.
  Auth: 'Auth@https://dev.myapp.com/auth/remoteEntry.js',
  Account: 'Account@https://dev.myapp.com/account/remoteEntry.js',
  Cart: 'Cart@https://dev.myapp.com/cart/remoteEntry.js',
  Support: 'Support@https://dev.myapp.com/support/remoteEntry.js',

  // Next.js remotes — remoteEntry.js inside _next/static/{ssr|chunks}/
  // These MFEs are built with NextFederationPlugin
  // isServer picks the SSR entry on server, browser entry on client
  Content: `Content@https://dev.myapp.com/content/_next/static/${
    isServer ? 'ssr' : 'chunks'
  }/remoteEntry.js`,
  Products: `Products@https://dev.myapp.com/products/_next/static/${
    isServer ? 'ssr' : 'chunks'
  }/remoteEntry.js`,
},

Next.js host remote loading flow diagram showing how remoteEntry.js files are fetched from React and Next.js remotes at runtime

React Remotes vs Next.js Remotes — Path Differences

Remote TyperemoteEntry.js PathisServer Needed?Why
React (Auth, Cart, Account, Support)/auth/remoteEntry.jsNoModuleFederationPlugin outputs one file at root — no SSR
Next.js (Content, Products)/content/_next/static/{ssr|chunks}/remoteEntry.jsYesNextFederationPlugin outputs two files — one for Node.js (SSR), one for browser

The isServer flag is passed by Next.js when it runs webpack. Next.js builds twice — once for the server (Node.js) and once for the client (browser). The isServer ? 'ssr' : 'chunks' ternary picks the correct remoteEntry.js for each build.

React remotes do NOT need the isServer check. They produce a single remoteEntry.js that works only in the browser. The host loads them with ssr: false via next/dynamic, so the server build never tries to fetch their remote entry.

Step 4 — Configure Shared Dependencies

Shared dependencies ensure that React, Redux, and your internal packages are loaded once and shared across all remotes. Without sharing, each remote bundles its own copy — multiplying bundle size and breaking singleton state.

apps/Main/next.config.js
// CORRECT — eager: false for all shared dependencies
shared: {
  react: {
    singleton: true,
    requiredVersion: false,
    eager: false,
  },
  'react-dom': {
    singleton: true,
    requiredVersion: false,
    eager: false,
  },
  'react-redux': {
    singleton: true,
    requiredVersion: false,
    eager: false,
  },
  '@reduxjs/toolkit': {
    singleton: true,
    requiredVersion: false,
    eager: false,
  },
  // Shared internal packages — strictVersion ensures exact match
  '@myapp/store': {
    singleton: true,
    strictVersion: true,
    requiredVersion: '1.0.0',
  },
  '@myapp/api': {
    singleton: true,
    strictVersion: true,
    requiredVersion: '1.0.0',
  },
  '@myapp/seo': {
    singleton: true,
    strictVersion: true,
    requiredVersion: '1.0.0',
  },
},

Key configuration choices:

  • requiredVersion: false for core libraries (react, react-dom) — Next.js manages its own React version internally. Pinning a specific version can conflict with Next.js's bundled React.
  • eager: false for everything — in Next.js, eager loading breaks SSR hydration because shared modules load synchronously before the hydration cycle completes. This is the most common source of "Cannot read properties of null" errors in Next.js MFE setups.
  • strictVersion: true + requiredVersion: '1.0.0' for internal packages — ensures that the host and all remotes use the exact same version of @myapp/store, @myapp/api, and @myapp/seo. A version mismatch means two store instances, breaking shared state.
  • No react-router-dom — Next.js uses its own next/router. React remotes that use React Router internally handle their own routing.

Step 5 — Configure Extra Options

The extraOptions object provides Next.js-specific features that do not exist in ModuleFederationPlugin. These options solve real problems that surface only when running Module Federation inside the Next.js framework.

apps/Main/next.config.js
// apps/Main/next.config.js — extraOptions (NextFederationPlugin only)
extraOptions: {
  // Expose all Next.js pages as federated modules automatically.
  // Without this, you must manually add each page to the exposes map.
  exposePages: true,

  // Fix next/image component when loaded from a federated remote.
  // Without this, images in remote components fail to resolve because
  // the image loader URL points to the host domain, not the remote domain.
  enableImageLoaderFix: true,

  // Fix static assets (fonts, SVGs) loaded via url-loader in remotes.
  // Without this, asset URLs resolve relative to the host publicPath —
  // returning 404 for every asset served by the remote.
  enableUrlLoaderFix: true,

  // Wrap exposed modules in an async boundary automatically.
  // This enables top-level await for shared dependency negotiation.
  // Without this, you need a manual bootstrap.js file like React MFEs use.
  automaticAsyncBoundary: true,
},

Step 6 — Transpile Shared Packages and Browser Fallbacks

Shared packages from the monorepo need explicit transpilation because Next.js treats symlinked workspace packages like node_modules — it skips them during compilation. You also need browser fallbacks for any Node.js core modules that shared packages import.

apps/Main/next.config.js
// apps/Main/next.config.js — transpilePackages and resolve
{
  // Tell Next.js to transpile shared packages from the monorepo.
  // Without this, Next.js skips packages in node_modules —
  // and monorepo symlinks look like node_modules to the bundler.
  transpilePackages: ['@myapp/seo', '@myapp/store', '@myapp/api'],

  // ...inside webpack(config, { isServer }) {}:

  // Add babel-loader for shared package TypeScript/JSX files
  config.module.rules.push({
    test: /\.(tsx|ts|jsx|js)$/,
    include: [
      /packages\/seo/,
      /packages\/store/,
      /packages\/api/,
    ],
    use: {
      loader: 'babel-loader',
      options: {
        presets: [
          '@babel/preset-react',
          '@babel/preset-typescript',
        ],
      },
    },
  });

  // Ensure all extensions are resolved
  config.resolve.extensions = ['.jsx', '.js', '.tsx', '.ts', '.json'];
}
apps/Main/next.config.js
// apps/Main/next.config.js — Browser fallback for Node.js modules
webpack(config, { isServer }) {
  // Some shared packages (e.g., @myapp/api) import Node.js core modules
  // like 'stream' or 'zlib' for compression/decompression.
  // These modules exist in Node.js but NOT in the browser.
  // Without these fallbacks, the client build crashes with:
  //   "Module not found: Can't resolve 'fs'"
  //   "Module not found: Can't resolve 'stream'"
  if (!isServer) {
    config.resolve.fallback = {
      ...config.resolve.fallback,
      fs: false,      // filesystem — not available in browser
      stream: false,  // Node.js streams — not needed client-side
      zlib: false,    // compression — handled by browser natively
    };
  }

  // ...rest of webpack config
  return config;
}
⚠️

Without transpilePackages, your shared packages will not be transpiled and the build will fail with syntax errors if they contain TypeScript or JSX. Without the browser fallbacks, the client build crashes with Module not found: Can't resolve 'fs' if any shared package imports Node.js modules conditionally.

Step 7 — Set Up _app.tsx with Shared Redux Provider

The _app.tsx file wraps every page in the application. This is where the host mounts the shared Redux store, the authentication initializer, the layout shell, and route protection.

apps/Main/pages/_app.tsx
// apps/Main/pages/_app.tsx
import React from 'react';
import type { AppProps } from 'next/app';
import dynamic from 'next/dynamic';
import '../styles/globals.css';

// Dynamic imports — client-side only, no SSR
const Layout = dynamic(() => import('../components/Layout'), {
  ssr: false,
  loading: () => <div className="shimmer" />,
});

const InitiateAuth = dynamic(() => import('../components/InitiateAuth'), {
  ssr: false,
  loading: () => null,
});

const ClientReduxProvider = dynamic(
  () => import('../components/ClientReduxProvider'),
  { ssr: false, loading: () => null }
);

const ProtectedRoute = dynamic(
  () => import('../components/Security/ProtectedRoute'),
  { ssr: false, loading: () => null }
);

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ClientReduxProvider>
      <InitiateAuth />
      <ProtectedRoute>
        <Layout>
          <Component {...pageProps} />
        </Layout>
      </ProtectedRoute>
    </ClientReduxProvider>
  );
}

export default MyApp;
apps/Main/components/ClientReduxProvider.tsx
// apps/Main/components/ClientReduxProvider.tsx
'use client';
import { ReactNode } from 'react';
import { Provider, store } from '@myapp/store';

interface ClientReduxProviderProps {
  children: ReactNode;
}

export default function ClientReduxProvider({ children }: ClientReduxProviderProps) {
  return (
    <Provider store={store}>
      {children}
    </Provider>
  );
}

// Why a separate component?
//
// 1. The 'use client' directive marks this as a Client Component.
//    Next.js App Router needs this to use React context (Provider).
//
// 2. @myapp/store is a shared singleton via Module Federation.
//    Every remote MFE that imports @myapp/store gets the SAME instance.
//    The host creates the store once — remotes reuse it.
//
// 3. The Provider wraps the entire app so every remote component
//    can call useSelector/useDispatch without creating their own store.

Every component in _app.tsx is loaded with next/dynamic and ssr: false. This ensures that client-only dependencies (Redux store, window-based auth checks, DOM-dependent layout) do not execute during server-side rendering. The loading order matters:

  1. ClientReduxProvider mounts first — creates the singleton store
  2. InitiateAuth checks for refresh tokens and restores the session
  3. ProtectedRoute blocks unauthenticated users from protected pages
  4. Layout renders the header, sidebar, and footer shell
  5. Component renders the actual page content (including remote MFE components)

Step 8 — Consume Remote Components

Remote MFE components are loaded at the page level. Each page imports the remote component using next/dynamic with ssr: false and wraps it with error handling.

pages/products/index.tsx
// pages/products/index.tsx — Loading a remote MFE with next/dynamic
import dynamic from 'next/dynamic';
import ErrorBoundary from '../../components/ErrorBoundary';

// next/dynamic wraps the Module Federation import
// ssr: false is MANDATORY — remote entry scripts need the window object
const RemoteProductsMFE = dynamic(
  () =>
    import('Products/ProductDetailPage').catch(() => {
      return { default: () => <div>Failed to load Products</div> };
    }),
  {
    loading: () => <div className="shimmer" />,
    ssr: false,
  }
);

export default function ProductsPage() {
  return (
    <ErrorBoundary>
      <RemoteProductsMFE />
    </ErrorBoundary>
  );
}

// Loading a React remote — same pattern, same ssr: false
// pages/auth/login.tsx
const RemoteAuth = dynamic(
  () => import('Auth/LoginPage').catch(() => {
    return { default: () => <div>Failed to load Auth</div> };
  }),
  { loading: () => <div className="shimmer" />, ssr: false }
);

Use next/dynamic over React.lazy for these reasons:

  1. Built-in SSR exclusionssr: false in one call vs double-wrapping with React.lazy
  2. Built-in loading stateloading prop replaces Suspense fallback
  3. Automatic code splitting — Next.js manages chunk names without webpackChunkName comments
  4. Consistent with Next.js patterns — the framework team recommends next/dynamic for all dynamic imports

For a deep dive into loading patterns and fallback UI, see Lazy Loading Micro Frontends with React Suspense.

Step 9 — Image Optimization

The Next.js host provides built-in image optimization (opens in a new tab) via next/image. Configure remotePatterns to allow images from your CDN, asset servers, and development hosts.

apps/Main/next.config.js
// apps/Main/next.config.js — Image optimization configuration
images: {
  // Allow Next.js Image component to load from these external domains
  remotePatterns: [
    { protocol: 'https', hostname: 'cdn.myapp.com', pathname: '/**' },
    { protocol: 'https', hostname: 'assets.myapp.com', pathname: '/**' },
    { protocol: 'https', hostname: 'ik.imagekit.io', pathname: '/**' },
    { protocol: 'https', hostname: '**.unsplash.com', pathname: '/**' },
    { protocol: 'http', hostname: 'localhost', pathname: '/**' },
  ],
  // Prefer AVIF (smaller) with WebP fallback
  formats: ['image/avif', 'image/webp'],
  // Responsive breakpoints for srcset generation
  deviceSizes: [640, 750, 828, 1080, 1200, 1920],
  imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  // Cache optimized images for 60 seconds minimum
  minimumCacheTTL: 60,
},

This configuration enables:

  • Automatic AVIF/WebP conversion — smaller file sizes with formats: ['image/avif', 'image/webp']
  • Responsive srcset generationdeviceSizes and imageSizes generate optimized variants for every screen size
  • CDN cachingminimumCacheTTL: 60 caches optimized images for at least 60 seconds

Remote MFE components that use next/image need enableImageLoaderFix: true in extraOptions. Without it, the image loader constructs URLs relative to the host domain instead of the remote's domain, causing 404s for every image loaded from a remote component.

Step 10 — PWA and Bundle Analyzer

The host wraps the Next.js config with two higher-order functions: withPWA for Progressive Web App support and withBundleAnalyzer for bundle size analysis.

apps/Main/next.config.js
// PWA configuration — wraps the entire next.config.js export
const withPWA = require('next-pwa')({
  dest: 'public',          // Service worker output directory
  register: true,          // Auto-register the service worker
  skipWaiting: true,       // Activate new SW immediately (no waiting)
  disable: process.env.NODE_ENV === 'development',  // Disable in dev mode
  buildExcludes: [/middleware-manifest\.json$/],     // Exclude from precache
});

// Usage: module.exports = withPWA({ ...nextConfig });
apps/Main/next.config.js
// Bundle analyzer — wraps the PWA-wrapped config
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',  // Only run when ANALYZE=true
});

// Final export — both wrappers compose around the Next.js config:
// module.exports = withBundleAnalyzer(withPWA({ ...nextConfig }));
//
// Run the analyzer:
//   ANALYZE=true npm run build
//   or use the "analyze" npm script:
//   npm run analyze

The final export composes both wrappers: withBundleAnalyzer(withPWA({ ...nextConfig })). This pattern is common in Next.js — each wrapper adds functionality without modifying the core config.

Step 11 — Security Headers

Content Security Policy (CSP) headers control which scripts, styles, images, and connections the browser allows. In a micro frontend architecture, CSP is especially important because the host loads JavaScript from multiple remote origins at runtime.

apps/Main/next.config.js
// apps/Main/next.config.js — Security headers (overview)
async headers() {
  return [
    {
      source: '/:path*',
      headers: [
        {
          key: 'Content-Security-Policy',
          value: [
            "default-src 'self'",
            // Scripts — allow self, inline (required for Next.js), eval (Module Federation)
            "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com",
            // Styles — allow self, inline (required for CSS-in-JS)
            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
            // Images — allow self, data URIs, and CDN domains
            "img-src 'self' data: https: http: blob:",
            // Fonts — allow self and Google Fonts
            "font-src 'self' data: https://fonts.gstatic.com",
            // API connections — allow self, localhost (dev), and API domain
            "connect-src 'self' http://localhost:* ws://localhost:* https://api.myapp.com",
            // Frames — allow self and payment gateways
            "frame-src 'self' https://checkout.razorpay.com",
            "object-src 'none'",
            "base-uri 'self'",
          ].join('; '),
        },
        { key: 'X-Content-Type-Options', value: 'nosniff' },
        { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
        { key: 'X-XSS-Protection', value: '1; mode=block' },
      ],
    },
  ];
},

Key CSP directives for Module Federation:

  • 'unsafe-eval' in script-src — Module Federation uses new Function() to evaluate remote modules at runtime. Without unsafe-eval, remote loading fails silently.
  • 'unsafe-inline' in script-src and style-src — Next.js injects inline scripts for page data and inline styles for CSS-in-JS. Both require unsafe-inline.
  • connect-src with localhost — development mode requires WebSocket connections (ws://localhost) for hot module replacement.

For a comprehensive guide to CSP in micro frontend architecture, see the upcoming Content Security Policy article.

Complete next.config.js

Here is the complete, production-ready next.config.js with every configuration assembled into one file. This is the actual pattern used in production with generic names.

apps/Main/next.config.js
// apps/Main/next.config.js — COMPLETE production-ready configuration
const { NextFederationPlugin } = require('@module-federation/nextjs-mf');
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});
const withPWA = require('next-pwa')({
  dest: 'public',
  register: true,
  skipWaiting: true,
  disable: process.env.NODE_ENV === 'development',
  buildExcludes: [/middleware-manifest\.json$/],
});

module.exports = withBundleAnalyzer(
  withPWA({
    reactStrictMode: false,
    output: 'standalone',

    typescript: { ignoreBuildErrors: true },
    eslint: { ignoreDuringBuilds: true },

    images: {
      remotePatterns: [
        { protocol: 'https', hostname: 'cdn.myapp.com', pathname: '/**' },
        { protocol: 'https', hostname: 'assets.myapp.com', pathname: '/**' },
        { protocol: 'https', hostname: 'ik.imagekit.io', pathname: '/**' },
        { protocol: 'https', hostname: '**.unsplash.com', pathname: '/**' },
        { protocol: 'http', hostname: 'localhost', pathname: '/**' },
      ],
      formats: ['image/avif', 'image/webp'],
      deviceSizes: [640, 750, 828, 1080, 1200, 1920],
      imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
      minimumCacheTTL: 60,
    },

    compress: true,
    transpilePackages: ['@myapp/seo', '@myapp/store', '@myapp/api'],

    async headers() {
      return [
        {
          source: '/:path*',
          headers: [
            {
              key: 'Content-Security-Policy',
              value: [
                "default-src 'self'",
                "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com",
                "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
                "img-src 'self' data: https: http: blob:",
                "font-src 'self' data: https://fonts.gstatic.com",
                "connect-src 'self' http://localhost:* ws://localhost:* https://api.myapp.com",
                "frame-src 'self' https://checkout.razorpay.com",
                "object-src 'none'",
                "base-uri 'self'",
              ].join('; '),
            },
            { key: 'X-Content-Type-Options', value: 'nosniff' },
            { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
            { key: 'X-XSS-Protection', value: '1; mode=block' },
          ],
        },
      ];
    },

    webpack(config, { isServer }) {
      if (!isServer) {
        config.resolve.fallback = {
          ...config.resolve.fallback,
          fs: false,
          stream: false,
          zlib: false,
        };
      }

      config.module.rules.push({
        test: /\.(tsx|ts|jsx|js)$/,
        include: [/packages\/seo/, /packages\/store/, /packages\/api/],
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-typescript'],
          },
        },
      });

      config.resolve.extensions = ['.jsx', '.js', '.tsx', '.ts', '.json'];

      config.plugins.push(
        new NextFederationPlugin({
          name: 'Main',
          filename: 'static/chunks/remoteEntry.js',
          remotes: {
            Auth: 'Auth@https://dev.myapp.com/auth/remoteEntry.js',
            Account: 'Account@https://dev.myapp.com/account/remoteEntry.js',
            Cart: 'Cart@https://dev.myapp.com/cart/remoteEntry.js',
            Support: 'Support@https://dev.myapp.com/support/remoteEntry.js',
            Content: `Content@https://dev.myapp.com/content/_next/static/${
              isServer ? 'ssr' : 'chunks'
            }/remoteEntry.js`,
            Products: `Products@https://dev.myapp.com/products/_next/static/${
              isServer ? 'ssr' : 'chunks'
            }/remoteEntry.js`,
          },
          shared: {
            react: { singleton: true, requiredVersion: false, eager: false },
            'react-dom': { singleton: true, requiredVersion: false, eager: false },
            'react-redux': { singleton: true, requiredVersion: false, eager: false },
            '@reduxjs/toolkit': { singleton: true, requiredVersion: false, eager: false },
            '@myapp/store': { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
            '@myapp/api': { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
            '@myapp/seo': { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
          },
          extraOptions: {
            exposePages: true,
            enableImageLoaderFix: true,
            enableUrlLoaderFix: true,
            automaticAsyncBoundary: true,
          },
        })
      );

      return config;
    },
  })
);

Running the Host Application

Terminal
# Start the host in development mode (port 5000)
npm run dev

# Build for production (standalone output)
npm run build

# Start the production server
npm run start

# Analyze bundle size (opens interactive treemap)
npm run analyze

Next.js Host vs React Host — Comparison

FeatureNext.js HostReact Host
PluginNextFederationPluginModuleFederationPlugin
Config filenext.config.jswebpack.config.js
Entry pointpages/_app.tsxsrc/bootstrap.js
Remote loadingnext/dynamic (ssr: false)React.lazy + Suspense
SSR supportYes (isServer toggle)No
Outputstandalone (for Docker/Node.js)Static files (for Nginx)
Image optimizationBuilt-in next/imageManual or third-party
PWAnext-pwa pluginworkbox-webpack-plugin
Security headersBuilt-in headers() configManual webpack or Nginx
Shared dep eagerMust be falseCan be true
remoteEntry.jsstatic/chunks/remoteEntry.jsremoteEntry.js (root)
RoutingFile-based (next/router)react-router-dom
Bundle analysis@next/bundle-analyzerwebpack-bundle-analyzer

Next.js host vs React host configuration comparison for micro frontend architecture

Common Mistakes

  1. Missing NEXT_PRIVATE_LOCAL_WEBPACK=true — Module Federation silently fails. Remotes appear to load but render nothing. No error in the console. Always set this flag in every npm script.

  2. Using eager: true for shared dependencies — Causes hydration mismatch errors in production. React loads twice — once eagerly during chunk evaluation, once during hydration. Use eager: false for all shared deps in Next.js.

  3. Using React-style paths for Next.js remotes — Writing Content@/content/remoteEntry.js instead of Content@/content/_next/static/chunks/remoteEntry.js. The file does not exist at the root — NextFederationPlugin places it inside _next/static/.

  4. Forgetting ssr: false in next/dynamic — Remote components attach to window, which does not exist on the server. Without ssr: false, the server build crashes with ReferenceError: window is not defined.

  5. Not setting output: 'standalone' — Without standalone mode, next build produces a build that depends on node_modules at runtime. Docker images become massive (500MB+) instead of the lightweight standalone output (~150MB).

  6. Skipping transpilePackages for shared packages — Monorepo packages with TypeScript or JSX fail to compile because Next.js treats them as pre-built node_modules.

What's Next

You now have a complete Next.js host application loading both React and Next.js remote micro frontends. The next article covers how to configure Next.js remote MFEs with basePath and assetPrefix — the settings that make subdirectory routing and asset serving work correctly for Next.js remotes.

← Back to NextFederationPlugin vs ModuleFederationPlugin

Continue to Next.js Remote MFE with basePath and assetPrefix →


Frequently Asked Questions

What is a Next.js micro frontend host app?

A Next.js micro frontend host app is the main application that uses NextFederationPlugin to load and orchestrate multiple remote micro frontend modules at runtime. It handles routing, shared state management via a singleton Redux store, SSR-aware remote loading using the isServer flag, and wraps the entire architecture with PWA support, image optimization, and security headers. The host creates the shell (header, footer, sidebar) while each remote provides the page content.

What is the difference between a Next.js host and a React host in Module Federation?

A Next.js host uses NextFederationPlugin in next.config.js with isServer checks for SSR support, next/dynamic for loading remotes, and extraOptions like exposePages and enableImageLoaderFix. A React host uses ModuleFederationPlugin in webpack.config.js, React.lazy with Suspense for loading remotes, and has no SSR capabilities. The Next.js host serves remoteEntry.js from _next/static/chunks/ while the React host serves it from the root path.

Can a Next.js host load both React and Next.js remote micro frontends?

Yes. A Next.js host can load both React remotes built with ModuleFederationPlugin and Next.js remotes built with NextFederationPlugin simultaneously. React remotes use a simple path like /auth/remoteEntry.js while Next.js remotes use _next/static/chunks/remoteEntry.js with the isServer toggle for SSR. All remotes are loaded with next/dynamic using ssr: false since remote entry scripts require the window object.

Why does the Next.js host need NEXT_PRIVATE_LOCAL_WEBPACK=true?

NEXT_PRIVATE_LOCAL_WEBPACK=true forces Next.js to use the locally installed webpack 5 instead of its bundled internal copy. Module Federation plugins hook into webpack's container system which requires access to the full webpack API. Without this flag, Next.js uses a stripped-down webpack build that does not expose the container plugin API, causing Module Federation configuration to silently fail — no errors appear, but remotes never load.

What are extraOptions in NextFederationPlugin?

extraOptions is a configuration object unique to NextFederationPlugin. exposePages automatically exposes all Next.js pages as federated modules. enableImageLoaderFix corrects next/image URLs when loaded from remotes. enableUrlLoaderFix fixes static asset paths for fonts and SVGs. automaticAsyncBoundary wraps modules in an async boundary for shared dependency negotiation, replacing the manual bootstrap.js pattern used in React MFEs. These options do not exist in ModuleFederationPlugin.

How do you consume remote components in a Next.js host app?

Remote components are consumed using next/dynamic with ssr: false. The dynamic import wraps the Module Federation import statement (e.g., import('Products/ProductDetailPage')), a loading prop provides fallback UI during fetch, and ssr: false prevents the remote from rendering during server-side rendering since remotes attach to the window object which does not exist on the server. Error handling is added via .catch() on the import and an ErrorBoundary wrapper component.