Routing and Navigation
Routing is critical in micro frontends. Unlike a monolithic SPA with a single router, micro frontend systems must coordinate routing across multiple independently developed applications. Poor routing design leads to full-page reloads, broken back-button behavior, and confusing user experiences.
This guide covers the application shell pattern, URL-based routing strategies, client-side routing coordination, cross-application navigation, and deep linking.
Why Routing Matters in Micro Frontends
Section titled “Why Routing Matters in Micro Frontends”In a micro frontend architecture:
- Each micro frontend may have its own router — React Router, Vue Router, Angular Router, etc.
- The shell must own top-level routing — Deciding which micro frontend to load for which URL
- Users expect seamless navigation — No jarring full-page reloads when moving between features
- Deep links and bookmarks must work —
/checkout/123should load the right micro frontend directly
Routing decisions are intertwined with your composition strategy. Client-side composition typically requires more sophisticated routing orchestration than server-side composition, where the server can route by URL and compose the appropriate fragments.
Application Shell Pattern
Section titled “Application Shell Pattern”The application shell is the minimal host that owns the top-level layout and routing. It loads micro frontends into designated slots based on the current URL.
Shell Owns Top-Level Routing
Section titled “Shell Owns Top-Level Routing”The shell parses the URL and decides which micro frontend(s) to load. Micro frontends handle their own sub-routes within their slot.
URL: /products/electronics/123/reviews
Shell routing: /products/* → Load Product micro frontend /checkout/* → Load Checkout micro frontend /account/* → Load Account micro frontend
Product micro frontend (sub-routing): /products/electronics/123/reviews → Show review tab /products/electronics/123/specs → Show specs tabExample URL Structure
Section titled “Example URL Structure”A well-designed URL structure maps cleanly to ownership:
| URL Path | Owner | Micro Frontend |
|---|---|---|
/ | Shell | Home/landing |
/products/* | Product Team | Product catalog, PDP |
/checkout/* | Checkout Team | Cart, checkout flow |
/account/* | Account Team | Profile, orders, settings |
/admin/* | Admin Team | Admin dashboard |
Path prefixes create clear ownership boundaries. See team autonomy for how teams own their route namespaces.
URL-Based Routing Strategies
Section titled “URL-Based Routing Strategies”Path-Based Routing
Section titled “Path-Based Routing”The most common approach: each micro frontend owns a path prefix.
https://example.com/team-a/dashboardhttps://example.com/team-a/settingshttps://example.com/team-b/cataloghttps://example.com/team-b/checkoutImplementation: The shell’s router matches the path prefix and loads the corresponding micro frontend. Sub-routes are passed to the micro frontend.
const routeConfig = [ { path: '/team-a/*', microFrontend: 'team-a' }, { path: '/team-b/*', microFrontend: 'team-b' }, { path: '/', microFrontend: 'home' },];Subdomain-Based Routing
Section titled “Subdomain-Based Routing”Each micro frontend (or team) gets its own subdomain.
https://team-a.example.com/dashboardhttps://team-b.example.com/cataloghttps://checkout.example.com/cartPros: Strong isolation, independent deployments per subdomain, simple load balancing
Cons: Cross-subdomain navigation feels like leaving the site, cookie/shared state complexity, DNS and SSL management
Client-Side Routing Considerations
Section titled “Client-Side Routing Considerations”History API and popstate Events
Section titled “History API and popstate Events”Modern SPAs use the History API. When the user clicks a link or uses the back/forward buttons, the shell must:
- React to
popstate(browser back/forward) - Update the URL without full reload (
history.pushState/history.replaceState) - Notify the active micro frontend of route changes
- Mount/unmount micro frontends as the user navigates
// Shell listens for popstate (browser back/forward)window.addEventListener('popstate', () => { const path = window.location.pathname; loadMicroFrontendForPath(path);});Coordinating Multiple Routers
Section titled “Coordinating Multiple Routers”When each micro frontend has its own router (e.g., React Router), conflicts arise:
- Duplicate history entries — Each router may push its own state
- Scroll restoration — Multiple routers can fight over scroll position
- Route change events — The shell needs to know when a micro frontend navigates internally
Approach: The shell owns the primary History API interaction. Micro frontends receive the current path as a prop and render accordingly, or use hash-based sub-routing to avoid conflicting with the shell.
Single-SPA Approach to Routing
Section titled “Single-SPA Approach to Routing”Single-SPA is a meta-framework that orchestrates multiple frameworks. It uses an “application” registry and “activity functions” to determine when each micro frontend is active:
import { registerApplication, start } from 'single-spa';
registerApplication({ name: 'product-catalog', app: () => import('@org/product-catalog'), activeWhen: ['/products'],});
registerApplication({ name: 'checkout', app: () => import('@org/checkout'), activeWhen: ['/checkout'],});
start();When the URL matches activeWhen, Single-SPA loads and mounts the application. It handles the routing lifecycle centrally.
Cross-Application Navigation
Section titled “Cross-Application Navigation”Navigating Between Micro Frontends Without Full Reloads
Section titled “Navigating Between Micro Frontends Without Full Reloads”Users should move from /products/123 to /checkout without a full page reload. The shell must:
- Parse the navigation target
- Unmount the current micro frontend (or keep it in DOM if same route tree)
- Load and mount the target micro frontend
- Update the URL via
history.pushState
Links between micro frontends should use normal <a href="..."> or router.push() that the shell intercepts, rather than window.location.href assignments that cause reloads.
Shared Navigation Components
Section titled “Shared Navigation Components”A shared header or sidebar may contain links to different micro frontends. Two patterns:
1. Shell-owned navigation: The shell renders the nav and uses its router for links. Micro frontends do not render nav links.
2. Event-based navigation: Micro frontends emit navigation events; the shell listens and performs the route change.
// Micro frontend emits navigation intentwindow.dispatchEvent(new CustomEvent('app:navigate', { detail: { path: '/checkout' }}));
// Shell listens and handleswindow.addEventListener('app:navigate', (e) => { history.pushState({}, '', e.detail.path); loadMicroFrontendForPath(e.detail.path);});Event-Based Communication for Route Changes
Section titled “Event-Based Communication for Route Changes”Publish-subscribe keeps the shell and micro frontends loosely coupled:
// Simple event bus for route changesconst routeBus = { listeners: new Set(), navigate(path) { history.pushState({}, '', path); this.listeners.forEach(fn => fn(path)); }, subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },};Deep Linking and Bookmarkability
Section titled “Deep Linking and Bookmarkability”Users expect to bookmark or share URLs like https://example.com/products/electronics/123/reviews and land directly on that view.
Requirements:
-
Server-side routing — If the server serves the shell for all routes (e.g., SPA fallback), the initial HTML load is the same. The shell parses the URL on load and mounts the correct micro frontend with the correct sub-route.
-
No client-only routes — Avoid routes that only exist after client-side JavaScript runs. The server (or CDN) must serve the shell for all valid URLs.
-
Sub-route passing — When loading
/products/electronics/123/reviews, the shell loads the Product micro frontend and passeselectronics/123/reviews(or the full path) so it can render the reviews tab.
Code Example: Application Shell Router
Section titled “Code Example: Application Shell Router”Below is a minimal application shell router pattern:
// Simple application shell router (pseudocode)const ROUTES = [ { path: /^\/products(\/.*)?$/, mf: 'product-catalog', basePath: '/products' }, { path: /^\/checkout(\/.*)?$/, mf: 'checkout', basePath: '/checkout' }, { path: /^\/account(\/.*)?$/, mf: 'account', basePath: '/account' }, { path: /^\/$/, mf: 'home', basePath: '/' },];
function getRouteForPath(path) { return ROUTES.find(r => r.path.test(path));}
async function loadMicroFrontendForPath(path) { const route = getRouteForPath(path); if (!route) return render404();
const subPath = path.replace(route.basePath, '') || '/';
// Unmount previous micro frontend const container = document.getElementById('app-shell'); container.innerHTML = '';
// Load and mount const { mount } = await import( /* @vite-ignore */ `${getMfUrl(route.mf)}/remoteEntry.js` ); mount(container, { path: subPath, basePath: route.basePath });}
// Intercept link clicks for in-app navigationdocument.addEventListener('click', (e) => { const a = e.target.closest('a[href^="/"]'); if (a && !a.target && !e.ctrlKey && !e.metaKey) { e.preventDefault(); loadMicroFrontendForPath(a.getAttribute('href')); history.pushState({}, '', a.getAttribute('href')); }});
window.addEventListener('popstate', () => loadMicroFrontendForPath(location.pathname));
// Initial loadloadMicroFrontendForPath(location.pathname);Summary
Section titled “Summary”- The application shell owns top-level routing and loads micro frontends based on URL.
- Path-based and subdomain-based routing define ownership boundaries.
- Coordinate client-side routing via History API, popstate, and event-based communication.
- Ensure deep linking works by serving the shell for all routes and passing sub-routes to micro frontends.
- Cross-application navigation should avoid full reloads; use the shell’s router or a shared event bus.
For how composition affects routing, see composition strategies. For organizational ownership of route namespaces, see team autonomy.
Go Deeper in the Book
This topic is covered in depth in Chapters 3-4 of Micro Frontends Architecture for Scalable Applications, with detailed examples, diagrams, and production-ready patterns.