Taming the Beast: Mastering Micro-Frontends with Webpack Module Federation & Next.js

Taming the Beast: Mastering Micro-Frontends with Webpack Module Federation & Next.js

January 28, 2025 · 4 min read

TL;DR

Webpack Module Federation lets independent teams deploy separate frontend apps that share code and compose into one UI at runtime. Use Next.js as the host for routing and auth, expose specific components from remote apps, and share React as a singleton to avoid version conflicts.

Webpack Module Federation was introduced in Webpack 5, released in October 2020, as a first-class feature for sharing code between separately deployed builds.

Webpack

Zalando, IKEA, and Spotify are among large enterprises publicly documented as using micro-frontend architectures to enable independent team deployments.

Micro Frontends (martinfowler.com)

The @module-federation/nextjs-mf package enables Module Federation support in Next.js, which does not include it by default.

Module Federation GitHub

Introduction: Why Micro-Frontends Matter Now

Modern web apps are evolving into sprawling ecosystems. Monolithic React codebases buckle under the weight of feature creep, conflicting team priorities, and deployment bottlenecks. Enter micro-frontends—a paradigm that decomposes your UI into independent, team-owned "apps within an app." But how do you glue them together without chaos?

The answer: Webpack Module Federation + Next.js. Let’s build a scalable architecture that balances autonomy with cohesion.


1. Module Federation 101: The Glue That Holds It All Together

Webpack Module Federation (WMF) lets apps dynamically load code from remote sources at runtime. Think of it as a microservice architecture for your frontend.

Real-World Example:
A travel booking platform splits its UI into:

  • search-app (Next.js, Team A)
  • booking-app (React + Vite, Team B)
  • user-profile-app (Remix, Team C)

Each team deploys independently, but WMF stitches them into a seamless experience.

Code Snippet: Basic WMF Setup

// webpack.config.js (Host App)
const { ModuleFederationPlugin } = require("webpack").container;
 
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "host_app",
      remotes: {
        search: "search_app@https://cdn.your-domain.com/remoteEntry.js",
        // include more.
      },
      shared: ["react", "react-dom"], // Avoid duplicate dependencies
    }),
  ],
};

Insight: Use shared to avoid loading React twice. Version mismatches? Webpack’s singleton: true enforces a single instance.


2. Next.js as the Orchestrator: Supercharge Your Host App

Next.js isn’t just for SSR—it’s a powerhouse for micro-frontend routing and performance.

Real-World Pattern:

  • Host App: Next.js handles routing, authentication, and global state.
  • Remotes: Feature apps (React, Vue, etc.) lazy-loaded via dynamic imports.

Code Snippet: Dynamic Remote Loading

// pages/index.tsx (Host App)
import dynamic from "next/dynamic";
 
const SearchApp = dynamic(() => import("search_app/SearchModule"), {
  loading: () => <p>Loading search...</p>,
  ssr: false,
});
 
const HomePage = () => (
  <div>
    <Header />
    <SearchApp /> {/* Rendered from remote! */}
  </div>
);

Insight: Disable SSR for remotes if they’re client-side only. Next.js 13+ with React Server Components? Plan carefully—not all remotes play nice with RSC yet.


3. Deployment: CI/CD Pipelines That Don’t Collide

Strategy 1: Independent Deployments

  • Each app has its own CI/CD pipeline.
  • Use semantic versioning for shared libraries.

Strategy 2: Coordinated Releases

  • Deploy the host app after remotes (e.g., booking-app v2.0 requires host-app v1.5).
  • Tools like Azure DevOps or GitHub Actions can automate dependency checks.

Real-World Nightmare (and Fix):
A retail app’s checkout page can break because the host app could cache an old remoteEntry.js.
Solution: Use CDNs with immutable filenames (e.g., remoteEntry-[hash].js).


4. Taming Shared Dependencies

Problem: Multiple React instances = silent crashes, hooks hell.
Solution: Enforce dependency alignment:

// In all webpack.config.js files
shared: {
  react: { singleton: true, requiredVersion: '^18.2.0' },
  'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
},

Pro Tip: Audit dependencies with npm ls react across all apps.


5. CSS Collisions: The Silent Killer

Problem: A .button class in search-app overrides styles in booking-app.

Solutions:

  • Scoped CSS: Use CSS Modules or styled-components.
  • Atomic CSS: Adopt Tailwind with strict naming conventions.
  • Shadow DOM: Extreme isolation for legacy apps.

Real-World Fix:
A fintech app used CSS-in-JS (Emotion) with unique classname hashes to eliminate collisions.


6. Cross-App State Sync: Beyond React Context

Problem: How does search-app notify booking-app that a date was selected?

Solution 1: Event Bus

// shared/eventBus.js
export const bus = new EventEmitter();
 
// In search-app
bus.emit("dateSelected", payload);
 
// In booking-app
bus.on("dateSelected", handleDate);

Solution 2: Zustand (Global Store)

// shared/store.js
import { create } from 'zustand';
 
export useSharedStore = create((set) => ({
  selectedDate: null,
  setDate: (date) => set({ selectedDate: date }),
}));

Insight: Avoid over-sharing. Not every state needs to be global!


7. Testing: Don’t Let the House of Cards Collapse

  • Contract Testing: Ensure remotes comply with host API expectations (use Pact).
  • Integration Tests: Run Cypress against a stitched production build.

Pro Tip: Mock remotes in the host’s Jest setup for faster unit tests.


Conclusion: Start Small, Scale with Confidence

Micro-frontends aren’t a silver bullet—they’re a tradeoff. Use them when:

  • Teams outgrow monolithic workflows.
  • Tech diversity is unavoidable (React + Angular + Vue).
  • You need incremental upgrades (migrate legacy code piecemeal).

Final Wisdom: "Micro-frontends solve organizational problems first, technical ones second."


🚀 Ready to Dive Deeper?

Got war stories or questions? Share them below! 👇

Frequently Asked Questions

What is Webpack Module Federation?

Module Federation is a Webpack 5 feature that allows a JavaScript application to dynamically load code from another separately deployed build at runtime. It enables true independent deployment of frontend modules without a shared build step.

How do micro-frontends work with Next.js?

Next.js acts as the host application handling routing, auth, and global layout. Remote apps expose components via Module Federation, and the host dynamically imports them. The @module-federation/nextjs-mf package adds the necessary webpack plugin support.

How do I avoid loading React twice with Module Federation?

Add react and react-dom to the shared array in ModuleFederationPlugin and set singleton: true. This forces all federated modules to use a single shared React instance, preventing hook errors and bundle bloat.

What is the difference between micro-frontends and monorepos?

A monorepo colocates code but still produces one build artifact per deployment. Micro-frontends go further — each app is deployed and versioned independently, with integration happening at runtime in the browser rather than at build time.

When should I not use micro-frontends?

Avoid them for small teams or applications, as the operational complexity (multiple deployments, version coordination, shared dependency management) outweighs the autonomy benefits. They pay off at organizations with multiple teams owning distinct product areas.

GitHub
LinkedIn
X