Micro-Frontends in Next.js (MFE) with Module Federation

August 27, 2025

Micro-Frontends in Next.js with Module Federation 

Micro-frontends (MFE) promise independent teams, faster deployments, and better scalability. But implementing them in Next.js comes with real tradeoffs. Here's what you need to know before jumping in.

What is Module Federation? 

Module Federation is a webpack feature that lets you share code between separate applications at runtime. Think of it as importing components from another deployed app, not from npm.

// Host app imports a component from Remote app
import RemoteComponent from 'remote/Component';

function Page() {
  return <RemoteComponent />;
}

The remote app exposes components, and the host app consumes them - all happening in the browser.

What Works 

✅ Client Components

You can federate any "use client" component:

"use client"

export default function ProductCard({ id }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    fetch(`https://api.products.com/items/${id}`)
      .then(res => res.json())
      .then(setProduct);
  }, [id]);

  return <div>{product?.name}</div>;
}

✅ Shared Libraries

Common utilities, hooks, and types can be shared:

// Expose
exposes: {
  './useAuth': './hooks/useAuth',
  './types': './types/index'
}

// Consume
import { useAuth } from 'auth-app/useAuth';

✅ Design Systems

Perfect for sharing UI components across teams:

exposes: {
  './Button': './components/Button',
  './Modal': './components/Modal'
}

What Doesn't Work 

❌ Server Components

Server Components run on the server and can't be federated. They need access to your database, environment variables, and the Next.js runtime.

// This CANNOT be federated
export default async function Page() {
  const posts = await db.query('SELECT * FROM posts');
  return <PostList posts={posts} />;
}

❌ API Routes

API routes are server endpoints, not webpack modules:

// Cannot federate
export async function GET() {
  return Response.json({ data: [] });
}

❌ Middleware

Middleware runs on the edge/server before requests reach your app. It's not a component you can share.

❌ Dynamic Routes with ISR

Next.js's [slug] routes with ISR (Incremental Static Regeneration) don't federate well. The routing happens in the host app's framework, not the remote's.

// This pattern doesn't federate
// app/posts/[slug]/page.tsx
export async function generateStaticParams() { ... }
export default async function Post({ params }) { ... }

Core Pain Points 

1. Server vs Client Boundary

The biggest confusion: federated components run in the host app's browser, not the remote app's server.

If your remote component needs data, it must fetch it via API calls back to the remote server:

"use client"

export default function RemoteComponent() {
  useEffect(() => {
    // Fetch from remote app's API
    fetch('https://remote-app.com/api/data')
      .then(res => res.json())
      .then(setData);
  }, []);
}

2. Type Safety

TypeScript types don't automatically sync between apps. You need to:

  • Share types through a package
  • Use API contracts
  • Accept some runtime risk

3. Versioning Hell

When Remote v2 breaks compatibility with Host v1, you get runtime errors in production. Solutions:

  • Semantic versioning for exposed modules
  • Strict API contracts
  • Extensive integration testing

4. Build Complexity

Each app needs webpack configuration for federation. Debugging spans multiple codebases. Local development requires running multiple apps.

5. Performance

Each federated module is a separate network request. Too many remotes = slow initial load.

Do's and Don'ts 

✅ DO

Share stable UI components - Design systems, common widgets, reusable forms

Use for large, independent features - Shopping cart, user profile, admin dashboard

Implement proper error boundaries - Remote components can fail to load

<ErrorBoundary fallback={<div>Feature unavailable</div>}>
  <RemoteComponent />
</ErrorBoundary>

Version your exposed modules - Breaking changes need version bumps

Set up shared dependencies - React, React-DOM should be singletons

shared: {
  react: { singleton: true, requiredVersion: false },
  'react-dom': { singleton: true, requiredVersion: false }
}

Test integration thoroughly - Unit tests won't catch cross-app issues

❌ DON'T

Don't federate for small apps - The complexity isn't worth it

Don't share server-side code - Database queries, env variables, file system access

Don't expect SSR to work seamlessly - Federated components typically need ssr: false

Don't forget about authentication - Each app needs to handle auth properly (use shared auth providers)

Don't ignore bundle sizes - Monitor what you're federating

Don't use it for code reuse alone - Just use npm packages

Authentication in MFE 

Both apps can have auth, but it requires coordination:

Best approach: Use a shared auth provider (Auth0, Clerk, Supabase)

// Both apps use same Auth0 tenant
const { user, getAccessToken } = useAuth0();

// Federated component makes authenticated API calls
const token = await getAccessToken();
fetch('https://remote-app.com/api/data', {
  headers: { 'Authorization': `Bearer ${token}` }
});

What doesn't work: Remote app's middleware won't protect federated components. The component runs in the host's context, not the remote's server.

When to Choose MFE 

Good Use Cases

Multiple teams, one product - Different teams own different sections (catalog, checkout, account)

Gradual migration - Migrating a monolith piece by piece

White-label products - Share core components, customize per client

Plugin architectures - Third parties can build extensions

Bad Use Cases

Small teams - Overhead outweighs benefits

Tight coupling - Features that need to share lots of state

Performance-critical apps - Extra network overhead matters

Simple blogs or marketing sites - Use monoliths or static generation

Alternatives to Consider 

Before going full MFE:

Monorepo with shared packages - Share code without runtime complexity (Turborepo, Nx)

Reverse proxy - Serve multiple apps under one domain without federation

// next.config.js
async rewrites() {
  return [
    { source: '/shop/:path*', destination: 'https://shop-app.com/:path*' }
  ];
}

Subdomain routing - shop.example.comblog.example.com - feels unified, stays simple

iframes - Sometimes the boring solution is the right solution

The Reality Check 

Module Federation is powerful but not magic. It trades runtime flexibility for build complexity, type safety, and debugging clarity.

Ask yourself:

  • Do we actually need runtime independence?
  • Can we solve this with a monorepo?
  • Is the team large enough to manage multiple deployments?
  • Are we okay with client-side only components?

If you're building a large-scale product with multiple teams that need to deploy independently, MFE might be worth it. If you're a small team building a standard web app, stick with simpler patterns.

Quick Decision Tree 

Do you have multiple autonomous teams?
  ├─ No → Don't use MFE
  └─ Yes
      └─ Do features need to share lots of state?
          ├─ Yes → Monorepo is better
          └─ No
              └─ Can features be deployed independently?
                  ├─ No → Monorepo is better
                  └─ Yes → Consider MFE

Final Thoughts 

Module Federation in Next.js works, but it's not as seamless as in pure React apps. The server/client boundary, ISR limitations, and App Router's architecture create friction.

Start simple. Use monorepos, reverse proxies, or subdomains first. Only reach for MFE when you have a clear organizational need for runtime independence.

HomeProjects
Blog
Contact

Building Experiences

Loading Creativity

Crafting Interfaces

Prepping UI

Almost Ready

Final Touches