PatternsMicrofrontends
Routing Orchestration
Managing navigation and routing across multiple microfrontends
Coordinating navigation across microfrontends is critical for seamless user experience. This guide covers proven routing patterns.
🎯 The Challenge
User navigates: /products/123 → /checkout
Questions:
- Who owns the router?
- How do MFEs know the current route?
- How to handle deep linking?
- How to prevent full page reloads?🛤️ Pattern 1: Shell-Based Routing (Recommended)
Shell application owns the router and mounts MFEs based on routes.
Implementation
// Shell App
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const HeaderMFE = lazy(() => import('header/App'));
const ProductsMFE = lazy(() => import('products/App'));
const CheckoutMFE = lazy(() => import('checkout/App'));
export function Shell() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<HeaderMFE />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/products/*" element={<ProductsMFE />} />
<Route path="/checkout/*" element={<CheckoutMFE />} />
<Route path="/account/*" element={<AccountMFE />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}MFE Internal Routing
// Products MFE
import { Routes, Route, useParams } from 'react-router-dom';
export function ProductsMFE() {
return (
<Routes>
<Route path="/" element={<ProductList />} />
<Route path="/:id" element={<ProductDetail />} />
<Route path="/category/:category" element={<CategoryPage />} />
</Routes>
);
}
// URL: /products/123
// Products MFE receives: /:id with params.id = "123"Pros:
- ✅ Single source of truth
- ✅ Clean URL structure
- ✅ Easy to debug
- ✅ Type-safe routing
Cons:
- ⚠️ Shell knows all routes
- ⚠️ Tight coupling to shell
🔀 Pattern 2: Distributed Routing
Each MFE manages its own routes independently.
Implementation
// Shell App
export function Shell() {
const [currentPath, setCurrentPath] = useState(window.location.pathname);
useEffect(() => {
// Listen to navigation events
const handlePopState = () => {
setCurrentPath(window.location.pathname);
};
window.addEventListener('popstate', handlePopState);
// Listen to custom navigation events
window.addEventListener('mfe-navigate', (e: CustomEvent) => {
setCurrentPath(e.detail.path);
});
return () => {
window.removeEventListener('popstate', handlePopState);
window.removeEventListener('mfe-navigate', handlePopState);
};
}, []);
return (
<div>
<HeaderMFE currentPath={currentPath} />
{currentPath.startsWith('/products') && <ProductsMFE />}
{currentPath.startsWith('/checkout') && <CheckoutMFE />}
{currentPath.startsWith('/account') && <AccountMFE />}
</div>
);
}Navigation Helper
// shared/navigation.ts
export function navigate(path: string) {
// Update browser history
window.history.pushState({}, '', path);
// Dispatch event to notify all MFEs
window.dispatchEvent(new CustomEvent('mfe-navigate', {
detail: { path }
}));
}
export function useCurrentPath() {
const [path, setPath] = useState(window.location.pathname);
useEffect(() => {
const handler = (e: CustomEvent) => {
setPath(e.detail.path);
};
window.addEventListener('mfe-navigate', handler as EventListener);
window.addEventListener('popstate', () => setPath(window.location.pathname));
return () => {
window.removeEventListener('mfe-navigate', handler as EventListener);
window.removeEventListener('popstate', () => setPath(window.location.pathname));
};
}, []);
return path;
}Usage in MFEs
// Products MFE
import { navigate, useCurrentPath } from '@company/shared';
export function ProductCard({ product }) {
const handleClick = () => {
navigate(`/products/${product.id}`);
};
return (
<div onClick={handleClick}>
{product.name}
</div>
);
}
export function ProductsMFE() {
const currentPath = useCurrentPath();
// Parse route manually
const productId = currentPath.match(/\/products\/(\d+)/)?.[1];
if (productId) {
return <ProductDetail id={productId} />;
}
return <ProductList />;
}Pros:
- ✅ Complete independence
- ✅ No shell coordination needed
- ✅ Easy to add new routes
Cons:
- ⚠️ Duplicate routing logic
- ⚠️ Hard to debug
- ⚠️ Route conflicts possible
🎯 Pattern 3: Hybrid Routing
Shell owns top-level routes, MFEs own internal routes.
// Shell - Top level only
<Routes>
<Route path="/products/*" element={<ProductsMFE />} />
<Route path="/checkout/*" element={<CheckoutMFE />} />
</Routes>
// Products MFE - Internal routes
<Routes>
<Route path="/" element={<ProductList />} />
<Route path="/:id" element={<ProductDetail />} />
<Route path="/category/:cat" element={<Category />} />
</Routes>
// URL Structure:
// /products → ProductList
// /products/123 → ProductDetail
// /products/category/electronics → Category🔗 Deep Linking
Handle direct navigation to nested routes.
// Shell App
export function Shell() {
// Parse initial URL on load
const initialPath = window.location.pathname;
return (
<BrowserRouter>
<Routes>
<Route path="/products/*" element={
<ProductsMFE initialPath={initialPath} />
} />
</Routes>
</BrowserRouter>
);
}
// Products MFE
export function ProductsMFE({ initialPath }: { initialPath: string }) {
// Extract product ID from initial path
const productId = initialPath.match(/\/products\/(\d+)/)?.[1];
// Fetch product data immediately
useEffect(() => {
if (productId) {
fetchProduct(productId);
}
}, [productId]);
return (
<Routes>
<Route path="/:id" element={<ProductDetail />} />
</Routes>
);
}🔄 Cross-MFE Navigation
Navigate between MFEs smoothly.
// shared/router.ts
class MFERouter {
navigate(path: string, options?: { replace?: boolean }) {
if (options?.replace) {
window.history.replaceState({}, '', path);
} else {
window.history.pushState({}, '', path);
}
// Notify all MFEs
window.dispatchEvent(new CustomEvent('route-change', {
detail: { path, action: options?.replace ? 'replace' : 'push' }
}));
}
back() {
window.history.back();
}
forward() {
window.history.forward();
}
getCurrentPath(): string {
return window.location.pathname;
}
subscribe(callback: (path: string) => void): () => void {
const handler = (e: CustomEvent) => {
callback(e.detail.path);
};
window.addEventListener('route-change', handler as EventListener);
window.addEventListener('popstate', () => callback(window.location.pathname));
return () => {
window.removeEventListener('route-change', handler as EventListener);
window.removeEventListener('popstate', () => callback(window.location.pathname));
};
}
}
export const mfeRouter = new MFERouter();Usage
// Header MFE - Navigation
import { mfeRouter } from '@company/shared';
export function Navigation() {
return (
<nav>
<a onClick={() => mfeRouter.navigate('/products')}>Products</a>
<a onClick={() => mfeRouter.navigate('/checkout')}>Checkout</a>
<a onClick={() => mfeRouter.navigate('/account')}>Account</a>
</nav>
);
}
// Products MFE - Link to checkout
export function ProductDetail() {
const handleCheckout = () => {
mfeRouter.navigate('/checkout');
};
return <button onClick={handleCheckout}>Checkout</button>;
}📍 Route Matching
Determine which MFE to render.
// Shell App
interface RouteConfig {
path: string;
mfe: string;
component: React.ComponentType;
}
const routes: RouteConfig[] = [
{ path: '/products', mfe: 'products', component: ProductsMFE },
{ path: '/checkout', mfe: 'checkout', component: CheckoutMFE },
{ path: '/account', mfe: 'account', component: AccountMFE },
];
export function Shell() {
const currentPath = useCurrentPath();
// Find matching route
const matchedRoute = routes.find(route =>
currentPath.startsWith(route.path)
);
if (!matchedRoute) {
return <NotFound />;
}
const Component = matchedRoute.component;
return <Component />;
}🎨 Active Route Highlighting
Highlight active navigation items.
// Header MFE
import { useCurrentPath } from '@company/shared';
export function Navigation() {
const currentPath = useCurrentPath();
const isActive = (path: string) => {
return currentPath.startsWith(path);
};
return (
<nav>
<a
className={isActive('/products') ? 'active' : ''}
onClick={() => navigate('/products')}
>
Products
</a>
<a
className={isActive('/checkout') ? 'active' : ''}
onClick={() => navigate('/checkout')}
>
Checkout
</a>
</nav>
);
}🔐 Protected Routes
Handle authentication across MFEs.
// Shell App
export function Shell() {
const { user, loading } = useAuth();
if (loading) return <LoadingSpinner />;
return (
<BrowserRouter>
<Routes>
<Route path="/products/*" element={<ProductsMFE />} />
{/* Protected routes */}
<Route path="/account/*" element={
user ? <AccountMFE /> : <Navigate to="/login" />
} />
<Route path="/admin/*" element={
user?.role === 'admin' ? <AdminMFE /> : <Navigate to="/" />
} />
</Routes>
</BrowserRouter>
);
}📊 Route Analytics
Track navigation events.
// shared/analytics.ts
class RouteAnalytics {
private previousPath: string = '';
init() {
mfeRouter.subscribe((path) => {
this.trackPageView(path);
});
}
trackPageView(path: string) {
// Send to analytics service
analytics.page({
path,
previousPath: this.previousPath,
timestamp: Date.now(),
referrer: document.referrer,
});
this.previousPath = path;
}
trackNavigation(from: string, to: string) {
analytics.track('navigation', {
from,
to,
duration: performance.now(),
});
}
}
export const routeAnalytics = new RouteAnalytics();⚡ Prefetching
Load MFEs before user navigates.
// Shell App
import { lazy, Suspense } from 'react';
const ProductsMFE = lazy(() => import('products/App'));
const CheckoutMFE = lazy(() => import('checkout/App'));
export function Navigation() {
// Prefetch on hover
const prefetchProducts = () => {
import('products/App');
};
const prefetchCheckout = () => {
import('checkout/App');
};
return (
<nav>
<a
onMouseEnter={prefetchProducts}
onClick={() => navigate('/products')}
>
Products
</a>
<a
onMouseEnter={prefetchCheckout}
onClick={() => navigate('/checkout')}
>
Checkout
</a>
</nav>
);
}🏢 Real-World Examples
Spotify
// Shell owns routes
/ → Home MFE
/search → Search MFE
/playlist/* → Playlist MFE
/artist/* → Artist MFE
// Prefetch on hover
// Analytics on every navigationAmazon
// Distributed routing
// Each widget (MFE) independent
// Deep linking fully supported
// Analytics per MFEIKEA
// Hybrid approach
// Shell: /products, /cart, /checkout
// MFEs: Internal routes
// Shared router utility📚 Key Takeaways
- Shell-based routing for most cases (simple, debuggable)
- Hybrid routing for independence (top-level + internal)
- Use history API not window.location (no page reload)
- Shared navigation utility prevents inconsistency
- Prefetch MFEs on hover for instant navigation
- Track all navigations for analytics
- Support deep linking from day one
Start simple with shell-based routing. Add complexity only when needed.