Field note / nextjs

publishedupdated 2026-05-01#module-federation#micro-frontend#webpack#nextjs

Module Federation at Scale

Webpack Module Federation as the runtime sharing layer for a micro-frontend platform — what works, what breaks, and how to recover.

Overview

Module Federation (Webpack 5) lets multiple independently-deployed apps share code at runtime — no npm publish cycle, no build-time coupling. One team ships a button update; every consumer gets it on the next page load.

Core concepts:

  • Remote — an app that exposes components/modules (exposes config)
  • Host — an app that consumes remote modules (remotes config)
  • Shared — dependencies (React, React-DOM) that must be singletons across the runtime

When it earns its complexity:

  • Multiple teams deploying independent frontend apps
  • Shared design system or auth components that change frequently
  • You want consumers updated without a build cycle

When to avoid it:

  • Single team, single repo — just import the component
  • SSR-critical above-fold content (MF is fundamentally client-side; SSR requires extra tooling)
  • Teams not disciplined about interface contracts

Technical Implementation

Remote Configuration

// design-system/next.config.js
const NextFederationPlugin = require('@module-federation/nextjs-mf');

module.exports = {
  webpack(config) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'designSystem',
        filename: 'static/chunks/remoteEntry.js',
        exposes: {
          './Button': './components/Button/index.tsx',
          './NavBar': './components/NavBar/index.tsx',
          './ProductCard': './components/ProductCard/index.tsx',
        },
        shared: {
          react: { singleton: true, requiredVersion: '^18.0.0', eager: false },
          'react-dom': { singleton: true, requiredVersion: '^18.0.0', eager: false },
        },
      })
    );
    return config;
  },
};

Host Configuration

// product-app/next.config.js
const NextFederationPlugin = require('@module-federation/nextjs-mf');

module.exports = {
  webpack(config) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'productHost',
        remotes: {
          designSystem: `designSystem@${process.env.DS_REMOTE_URL}/_next/static/chunks/remoteEntry.js`,
          auth: `auth@${process.env.AUTH_REMOTE_URL}/_next/static/chunks/remoteEntry.js`,
        },
        shared: {
          react: { singleton: true, requiredVersion: '^18.0.0' },
          'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
        },
      })
    );
    return config;
  },
};

Consuming a Remote — With Fallback

// product-app/components/AddToCart.tsx
import dynamic from 'next/dynamic';

// Always provide a fallback — remotes WILL 503 in production
const RemoteButton = dynamic(
  () =>
    import('designSystem/Button').catch(() => {
      return import('./FallbackButton');  // local fallback component
    }),
  {
    ssr: false,
    loading: () => <div className="h-10 w-32 animate-pulse bg-secondary rounded-md" />,
  }
);

export function AddToCartButton({ productId }: { productId: string }) {
  return (
    <RemoteButton variant="primary" onClick={() => addToCart(productId)}>
      Add to Cart
    </RemoteButton>
  );
}

TypeScript Contract Types

MF has no build-time type checking for remotes. The pattern that worked:

// @platform/mf-contracts (shared package, published on each remote release)
declare module 'designSystem/Button' {
  interface ButtonProps {
    variant: 'primary' | 'secondary' | 'ghost';
    size?: 'sm' | 'md' | 'lg';
    disabled?: boolean;
    onClick?: () => void;
    children: React.ReactNode;
  }
  const Button: React.FC<ButtonProps>;
  export default Button;
}

Architecture

MF loading sequence: SSR shell renders first, remotes load client-side in parallel

Challenges

1. React singleton violations

Early on, a remote shipped react@18.3.0 while the host was on 18.2.0. With singleton: true, Webpack chose 18.3.0. Hooks broke in the host because it thought it was running two React instances (a known React constraint).

Fix: CI check that fails a remote deploy if its react version differs from the platform's pinned version by more than a patch.

2. SSR hydration mismatches

Remote components rendered server-side via @module-federation/node but the client received a different version of the remote. Hydration failed silently, causing layout jumps.

Fix: disabled SSR for all MF components (ssr: false). Shipped skeletons for above-fold content. Accepted the CLS cost as acceptable for the operational simplicity.

3. remoteEntry.js cache invalidation

With Cloudfront aggressive caching, remoteEntry.js was served stale for up to 24h after a remote deploy. Consumers ran old code silently.

Fix: added remoteEntry.js?v=${BUILD_ID} as a cache-busting query param, configured in the host's environment variable pipeline.


Platform Ownership Checklist

  • Define the remote/host topology, shared dependency contracts, and fallback strategy.
  • Treat design-system remotes as versioned platform products with explicit compatibility guarantees.
  • Publish TypeScript declarations through a shared contracts package such as @platform/mf-contracts.
  • Monitor React singleton violations, hydration mismatches, and remote loading failures in production.
  • Keep the integration guide, compatibility matrix, and rollback procedure current.

References