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.com, blog.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.