Micro-Frontends: The Complete Architecture Guide for 2026
Learn how to build scalable micro-frontend applications. Complete guide covering Module Federation, routing, state management, and deployment strategies.
Micro-frontends promise to solve the scaling challenges of large frontend applications. After implementing dozens of micro-frontend architectures, here's what actually works, what doesn't, and how to build them right.
Related reading: Check out our guides on React performance optimization and design systems at scale for more architecture insights.
What Are Micro-Frontends?#
The Core Concept#
Micro-frontends extend the microservices concept to frontend development. Instead of a monolithic frontend application, you build multiple smaller, independent applications that work together as a cohesive user experience.
Key principles:
- Technology agnostic: Each micro-frontend can use different frameworks
- Independent deployment: Deploy parts of your app independently
- Team autonomy: Different teams own different parts of the application
- Isolated development: Develop and test in isolation
When Micro-Frontends Make Sense#
Large organizations with multiple teams working on the same product Legacy modernization where you need to gradually migrate old systems Different technology requirements for different parts of your application Independent release cycles for different features
When to Avoid Micro-Frontends#
Small teams (< 10 developers) - the overhead isn't worth it Simple applications - monoliths are often better for straightforward apps Tight coupling requirements - when features need deep integration Performance-critical applications - the overhead can impact performance
Micro-Frontend Implementation Strategies#
1. Module Federation (Webpack 5)#
Best for: React/Vue/Angular applications with modern build tools
// Host application webpack config
const ModuleFederationPlugin = require('@module-federation/webpack');
module.exports = {
mode: 'development',
devServer: {
port: 3000,
},
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
mfShell: 'shell@http://localhost:3001/remoteEntry.js',
mfProducts: 'products@http://localhost:3002/remoteEntry.js',
mfCheckout: 'checkout@http://localhost:3003/remoteEntry.js',
},
}),
],
};
// Remote application webpack config
module.exports = {
mode: 'development',
devServer: {
port: 3001,
},
plugins: [
new ModuleFederationPlugin({
name: 'shell',
filename: 'remoteEntry.js',
exposes: {
'./Header': './src/components/Header',
'./Navigation': './src/components/Navigation',
'./Footer': './src/components/Footer',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};
Using remote components:
// Host application
import React, { Suspense } from 'react';
const Header = React.lazy(() => import('mfShell/Header'));
const ProductList = React.lazy(() => import('mfProducts/ProductList'));
const Checkout = React.lazy(() => import('mfCheckout/CheckoutForm'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading header...</div>}>
<Header />
</Suspense>
<main>
<Suspense fallback={<div>Loading products...</div>}>
<ProductList />
</Suspense>
<Suspense fallback={<div>Loading checkout...</div>}>
<Checkout />
</Suspense>
</main>
</div>
);
}
2. Single-SPA Framework#
Best for: Multi-framework applications or gradual migration
// Root config
import { registerApplication, start } from 'single-spa';
// Register micro-frontends
registerApplication({
name: 'navbar',
app: () => import('./navbar/navbar.app.js'),
activeWhen: () => true, // Always active
});
registerApplication({
name: 'products',
app: () => import('./products/products.app.js'),
activeWhen: location => location.pathname.startsWith('/products'),
});
registerApplication({
name: 'checkout',
app: () => import('./checkout/checkout.app.js'),
activeWhen: '/checkout',
});
start();
// Individual micro-frontend (React)
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import ProductApp from './ProductApp';
const lifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: ProductApp,
errorBoundary(err, info, props) {
return <div>Error in products app</div>;
},
});
export const { bootstrap, mount, unmount } = lifecycles;
// Individual micro-frontend (Vue)
import Vue from 'vue';
import singleSpaVue from 'single-spa-vue';
import CheckoutApp from './CheckoutApp.vue';
const vueLifecycles = singleSpaVue({
Vue,
appOptions: {
render: h => h(CheckoutApp),
},
});
export const { bootstrap, mount, unmount } = vueLifecycles;
3. Web Components Approach#
Best for: Framework-agnostic solutions with maximum isolation
// Micro-frontend as Web Component
class ProductCatalog extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.loadProducts();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
padding: 20px;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
</style>
<div class="product-catalog">
<h2>Products</h2>
<div class="product-grid" id="products"></div>
</div>
`;
}
async loadProducts() {
const response = await fetch('/api/products');
const products = await response.json();
this.renderProducts(products);
}
renderProducts(products) {
const grid = this.shadowRoot.getElementById('products');
grid.innerHTML = products.map(product => `
<div class="product-card">
<h3>${product.name}</h3>
<p>$${product.price}</p>
<button onclick="this.addToCart('${product.id}')">Add to Cart</button>
</div>
`).join('');
}
addToCart(productId) {
// Dispatch custom event for communication
this.dispatchEvent(new CustomEvent('add-to-cart', {
detail: { productId },
bubbles: true,
}));
}
}
customElements.define('product-catalog', ProductCatalog);
// Usage in host application
document.addEventListener('add-to-cart', (event) => {
console.log('Product added to cart:', event.detail.productId);
// Update cart state
});
4. Server-Side Composition#
Best for: SEO-critical applications with server-side rendering
// Express.js composition server
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// Proxy to micro-frontends
app.use('/products', createProxyMiddleware({
target: 'http://products-service:3001',
changeOrigin: true,
}));
app.use('/checkout', createProxyMiddleware({
target: 'http://checkout-service:3002',
changeOrigin: true,
}));
// Server-side composition
app.get('/', async (req, res) => {
try {
// Fetch fragments from micro-frontends
const [header, products, footer] = await Promise.all([
fetch('http://shell-service:3000/header').then(r => r.text()),
fetch('http://products-service:3001/featured').then(r => r.text()),
fetch('http://shell-service:3000/footer').then(r => r.text()),
]);
const html = `
<!DOCTYPE html>
<html>
<head>
<title>E-commerce App</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
${header}
<main>
${products}
</main>
${footer}
<script src="/app.js"></script>
</body>
</html>
`;
res.send(html);
} catch (error) {
res.status(500).send('Error loading page');
}
});
app.listen(3000);
Communication Between Micro-Frontends#
1. Custom Events (Recommended)#
// Publishing events
class EventBus {
static dispatch(eventName, data) {
const event = new CustomEvent(eventName, {
detail: data,
bubbles: true,
});
document.dispatchEvent(event);
}
static subscribe(eventName, callback) {
document.addEventListener(eventName, callback);
// Return unsubscribe function
return () => document.removeEventListener(eventName, callback);
}
}
// Micro-frontend A (Products)
function addToCart(product) {
EventBus.dispatch('cart:add', { product });
}
// Micro-frontend B (Cart)
EventBus.subscribe('cart:add', (event) => {
const { product } = event.detail;
updateCartState(product);
});
2. Shared State Management#
// Shared store using RxJS
import { BehaviorSubject } from 'rxjs';
class SharedStore {
constructor() {
this.state$ = new BehaviorSubject({
user: null,
cart: [],
theme: 'light',
});
}
getState() {
return this.state$.value;
}
setState(newState) {
this.state$.next({ ...this.getState(), ...newState });
}
subscribe(callback) {
return this.state$.subscribe(callback);
}
// Specific actions
addToCart(product) {
const currentState = this.getState();
this.setState({
cart: [...currentState.cart, product],
});
}
setUser(user) {
this.setState({ user });
}
}
// Global instance
window.sharedStore = new SharedStore();
// Usage in micro-frontends
const store = window.sharedStore;
// Subscribe to changes
store.subscribe((state) => {
console.log('State updated:', state);
updateUI(state);
});
// Update state
store.addToCart(product);
3. URL-Based Communication#
// Router service for micro-frontends
class MicroFrontendRouter {
constructor() {
this.routes = new Map();
this.currentRoute = null;
window.addEventListener('popstate', this.handleRouteChange.bind(this));
}
register(pattern, microfrontend) {
this.routes.set(pattern, microfrontend);
}
navigate(path, state = {}) {
history.pushState(state, '', path);
this.handleRouteChange();
}
handleRouteChange() {
const path = window.location.pathname;
for (const [pattern, microfrontend] of this.routes) {
if (this.matchRoute(pattern, path)) {
this.activateMicrofrontend(microfrontend, path);
break;
}
}
}
matchRoute(pattern, path) {
const regex = new RegExp(pattern.replace(/:\w+/g, '([^/]+)'));
return regex.test(path);
}
activateMicrofrontend(microfrontend, path) {
if (this.currentRoute !== microfrontend) {
this.currentRoute?.deactivate?.();
microfrontend.activate(path);
this.currentRoute = microfrontend;
}
}
}
// Usage
const router = new MicroFrontendRouter();
router.register('/products/:category?', {
activate: (path) => {
import('./products/app.js').then(app => app.mount());
},
deactivate: () => {
// Cleanup
},
});
router.register('/checkout', {
activate: () => {
import('./checkout/app.js').then(app => app.mount());
},
});
Styling and Design Systems#
CSS Isolation Strategies#
/* BEM methodology for namespace isolation */
.mf-products__card {
border: 1px solid #ddd;
padding: 16px;
}
.mf-products__card--featured {
border-color: #007bff;
}
/* CSS Modules */
.productCard {
composes: card from 'shared-styles/components.css';
border-radius: 8px;
}
/* Styled Components with namespace */
const ProductCard = styled.div`
border: 1px solid #ddd;
padding: 16px;
&.mf-products-featured {
border-color: #007bff;
}
`;
Shared Design System#
// Design system package
// packages/design-system/src/index.js
export { Button } from './components/Button';
export { Card } from './components/Card';
export { theme } from './theme';
// Micro-frontend usage
import { Button, Card, theme } from '@company/design-system';
import { ThemeProvider } from 'styled-components';
function ProductApp() {
return (
<ThemeProvider theme={theme}>
<Card>
<h2>Product Name</h2>
<Button variant="primary">Add to Cart</Button>
</Card>
</ThemeProvider>
);
}
Testing Micro-Frontends#
Unit Testing#
// Jest configuration for micro-frontend
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
moduleNameMapping: {
'^@shared/(.*)$': '<rootDir>/../shared/src/$1',
},
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
},
};
// Testing with mocked dependencies
import { render, screen } from '@testing-library/react';
import ProductList from './ProductList';
// Mock external micro-frontend
jest.mock('mfCart/CartService', () => ({
addToCart: jest.fn(),
}));
test('renders product list', () => {
render(<ProductList />);
expect(screen.getByText('Products')).toBeInTheDocument();
});
Integration Testing#
// Cypress integration tests
describe('Micro-frontend Integration', () => {
it('should communicate between products and cart', () => {
cy.visit('/');
// Interact with products micro-frontend
cy.get('[data-testid="product-card"]').first().click();
cy.get('[data-testid="add-to-cart"]').click();
// Verify cart micro-frontend updates
cy.get('[data-testid="cart-count"]').should('contain', '1');
// Navigate to cart
cy.get('[data-testid="cart-link"]').click();
cy.url().should('include', '/cart');
cy.get('[data-testid="cart-item"]').should('exist');
});
});
Contract Testing#
// Pact.js for API contract testing
import { Pact } from '@pact-foundation/pact';
const provider = new Pact({
consumer: 'products-microfrontend',
provider: 'products-api',
port: 1234,
});
describe('Products API Contract', () => {
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
it('should get product list', async () => {
await provider.addInteraction({
state: 'products exist',
uponReceiving: 'a request for products',
withRequest: {
method: 'GET',
path: '/api/products',
},
willRespondWith: {
status: 200,
body: [
{ id: 1, name: 'Product 1', price: 99.99 },
],
},
});
const response = await fetch('http://localhost:1234/api/products');
const products = await response.json();
expect(products).toHaveLength(1);
expect(products[0]).toHaveProperty('id', 1);
});
});
Deployment and DevOps#
Container-Based Deployment#
# Dockerfile for micro-frontend
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# docker-compose.yml
version: '3.8'
services:
shell:
build: ./shell
ports:
- "3000:80"
environment:
- PRODUCTS_URL=http://products:80
- CHECKOUT_URL=http://checkout:80
products:
build: ./products
ports:
- "3001:80"
checkout:
build: ./checkout
ports:
- "3002:80"
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- shell
- products
- checkout
Kubernetes Deployment#
# k8s/products-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: products-microfrontend
spec:
replicas: 3
selector:
matchLabels:
app: products-microfrontend
template:
metadata:
labels:
app: products-microfrontend
spec:
containers:
- name: products
image: company/products-microfrontend:latest
ports:
- containerPort: 80
env:
- name: API_URL
value: "https://api.company.com"
---
apiVersion: v1
kind: Service
metadata:
name: products-service
spec:
selector:
app: products-microfrontend
ports:
- port: 80
targetPort: 80
type: ClusterIP
CI/CD Pipeline#
# .github/workflows/deploy.yml
name: Deploy Micro-frontend
on:
push:
branches: [main]
paths: ['products/**']
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
working-directory: ./products
- run: npm test
working-directory: ./products
- run: npm run test:integration
working-directory: ./products
build-and-deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: |
docker build -t company/products-microfrontend:${{ github.sha }} ./products
docker tag company/products-microfrontend:${{ github.sha }} company/products-microfrontend:latest
- name: Deploy to staging
run: |
kubectl set image deployment/products-microfrontend products=company/products-microfrontend:${{ github.sha }}
kubectl rollout status deployment/products-microfrontend
Performance Optimization#
Bundle Optimization#
// Webpack optimization for micro-frontends
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
shared: {
name: 'shared',
chunks: 'all',
minChunks: 2,
},
},
},
},
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},
};
// Dynamic imports for lazy loading
const LazyProductList = React.lazy(() =>
import('mfProducts/ProductList').catch(() => ({
default: () => <div>Products unavailable</div>
}))
);
Caching Strategies#
// Service worker for micro-frontend caching
const CACHE_NAME = 'mf-products-v1';
const REMOTE_CACHE = 'mf-remotes-v1';
self.addEventListener('fetch', event => {
const { request } = event;
// Cache micro-frontend bundles
if (request.url.includes('remoteEntry.js')) {
event.respondWith(
caches.open(REMOTE_CACHE).then(cache => {
return cache.match(request).then(response => {
if (response) {
// Serve from cache, update in background
fetch(request).then(fetchResponse => {
cache.put(request, fetchResponse.clone());
});
return response;
}
return fetch(request).then(fetchResponse => {
cache.put(request, fetchResponse.clone());
return fetchResponse;
});
});
})
);
}
});
Common Pitfalls and Solutions#
1. Dependency Conflicts#
Problem: Different versions of shared libraries causing conflicts
Solution: Use Module Federation's shared dependencies
// webpack.config.js
new ModuleFederationPlugin({
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0',
},
},
});
2. Performance Overhead#
Problem: Loading multiple bundles impacts performance
Solution: Implement smart loading strategies
// Preload critical micro-frontends
const preloadMicrofrontend = (name) => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = `${name}/remoteEntry.js`;
link.as = 'script';
document.head.appendChild(link);
};
// Preload on user interaction
button.addEventListener('mouseenter', () => {
preloadMicrofrontend('products');
});
3. State Management Complexity#
Problem: Sharing state between micro-frontends becomes complex
Solution: Use event-driven architecture with clear boundaries
// Clear state boundaries
class MicrofrontendState {
constructor(namespace) {
this.namespace = namespace;
this.state = new Map();
}
get(key) {
return this.state.get(`${this.namespace}:${key}`);
}
set(key, value) {
this.state.set(`${this.namespace}:${key}`, value);
this.emit('stateChange', { key, value });
}
emit(event, data) {
window.dispatchEvent(new CustomEvent(`${this.namespace}:${event}`, {
detail: data
}));
}
}
Frequently Asked Questions#
Q: When should I choose micro-frontends over a monolith? A: Consider micro-frontends when you have multiple teams (10+ developers), need independent deployments, or are modernizing legacy systems. For smaller teams or simple applications, monoliths are often better.
Q: How do I handle SEO with micro-frontends? A: Use server-side composition or static site generation. Each micro-frontend should be able to render on the server and provide proper meta tags and structured data.
Q: What about bundle size and performance? A: Micro-frontends can increase bundle size due to duplication. Use shared dependencies, lazy loading, and proper caching strategies. Monitor performance metrics closely.
Q: How do I test micro-frontends? A: Use a combination of unit tests for individual micro-frontends, integration tests for communication, and contract tests for APIs. Consider using tools like Pact for contract testing.
Q: Can I mix different frameworks? A: Yes, that's one of the benefits of micro-frontends. You can use React for one part, Vue for another, and Angular for a third. However, this increases complexity and bundle size.
Micro-frontends are powerful but complex. They solve real problems for large organizations but introduce new challenges. Choose them when the benefits outweigh the costs, and implement them thoughtfully with proper tooling and processes.
Continue Reading
React Performance Optimization: 15 Proven Techniques 2026
Complete guide to React performance optimization. 15 proven techniques to make your React apps faster, including lazy loading, memoization, and code splitting.
GraphQL vs REST: The Definitive API Design Guide for 2026
Discover when to use GraphQL vs REST for your API. Includes performance comparisons, implementation examples, and best practices for modern API design.
WebAssembly vs JavaScript: The Performance Revolution in 2026
WebAssembly is reshaping web development. Learn when and how to use WASM for maximum performance gains, with practical examples and real-world case studies.
Browse by Topic
Find stories that matter to you.