Micro-frontends extend the microservices concept to frontend development, enabling teams to work independently on different parts of the same application. Here's how to implement this architecture effectively.
What Are Micro-Frontends?
Micro-frontends break down monolithic frontend applications into smaller, semi-independent applications that different teams can develop, test, and deploy separately.
// Traditional monolithic app
<app>
<header />
<sidebar />
<dashboard />
<reports />
<settings />
</app>
// Micro-frontends approach
<app>
<HeaderMicroFrontend />
<SidebarMicroFrontend />
<DashboardMicroFrontend />
<ReportsMicroFrontend />
<SettingsMicroFrontend />
</app>Core Principles
Team Autonomy
Each team owns their micro-frontend end-to-end.
// Team Checkout owns checkout module
// Team Dashboard owns dashboard module
// Team Search owns search module
// Independent deployment
checkout: https://checkout.example.com
dashboard: https://dashboard.example.com
search: https://search.example.comTechnology Agnostic
Different teams can use different frameworks.
// Team A: React
<CheckoutReactApp />
// Team B: Vue
<DashboardVueApp />
// Team C: Angular
<SearchAngularApp />Independent Deployment
Deploy without coordinating with other teams.
# Team Checkout CI/CD
deploy-checkout:
script:
- npm run build
- npm run test
- npm run deploy:checkout
only:
changes:
- packages/checkout/**/*Integration Patterns
Build-Time Integration
Combine during build process.
// webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'container',
remotes: {
checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
dashboard: 'dashboard@https://dashboard.example.com/remoteEntry.js',
},
}),
],
};Run-Time Integration
Load dynamically at runtime.
// Dynamic import
const loadMicroFrontend = async (name, url) => {
const module = await System.import(url);
return module.default;
};
// Usage in React
function MicroFrontend({ name, url }) {
const [Component, setComponent] = useState(null);
useEffect(() => {
loadMicroFrontend(name, url).then(setComponent);
}, [name, url]);
return Component ? <Component /> : <Loading />;
}Server-Side Integration
Compose on the server before sending to client.
// Next.js example
export async function getServerSideProps() {
const [checkout, dashboard] = await Promise.all([
fetch('https://checkout.example.com/api/ssr').then(r => r.text()),
fetch('https://dashboard.example.com/api/ssr').then(r => r.text()),
]);
return {
props: {
checkoutHtml: checkout,
dashboardHtml: dashboard,
},
};
}Module Federation
Webpack Module Federation makes micro-frontends easier.
// Host app (container)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'container',
remotes: {
products: 'products@https://products.example.com/remoteEntry.js',
cart: 'cart@https://cart.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
// Remote app (products)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./ProductsApp': './src/App',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};Using Remote Modules
// In host app
import ProductsApp from 'products/ProductsApp';
import CartApp from 'cart/CartApp';
function App() {
return (
<div>
<ProductsApp />
<CartApp />
</div>
);
}Communication Patterns
Custom Events
Loose coupling between micro-frontends.
// Publisher (Checkout micro-frontend)
window.dispatchEvent(new CustomEvent('cart-updated', {
detail: { itemCount: 5, total: 99.99 }
}));
// Subscriber (Header micro-frontend)
useEffect(() => {
const handleCartUpdate = (event) => {
const { itemCount, total } = event.detail;
updateCartBadge(itemCount, total);
};
window.addEventListener('cart-updated', handleCartUpdate);
return () => {
window.removeEventListener('cart-updated', handleCartUpdate);
};
}, []);Shared State Library
Use a global state manager.
// Using Redux with context
import { Provider } from 'react-redux';
import store from './store';
function MicroFrontendRoot({ children }) {
return (
<Provider store={store}>
{children}
</Provider>
);
}
// Shared state slices
export const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], total: 0 },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload);
state.total += action.payload.price;
},
},
});Message Bus
Event-driven communication.
// EventBus implementation
class EventBus {
constructor() {
this.events = {};
}
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
publish(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}
}
const eventBus = new EventBus();
// Usage in micro-frontends
eventBus.subscribe('user:login', (user) => {
updateUI(user);
});
eventBus.publish('user:login', userData);Routing Strategies
Client-Side Routing
Each micro-frontend handles its own routes.
// Using React Router
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/products/*" element={<ProductsMicroFrontend />} />
<Route path="/checkout/*" element={<CheckoutMicroFrontend />} />
<Route path="/dashboard/*" element={<DashboardMicroFrontend />} />
</Routes>
</BrowserRouter>
);
}
// Inside ProductsMicroFrontend
function ProductsMicroFrontend() {
return (
<Routes>
<Route path="/" element={<ProductList />} />
<Route path="/:id" element={<ProductDetail />} />
</Routes>
);
}Server-Side Routing
Route requests to different micro-frontends.
// Next.js custom server
const express = require('express');
const next = require('next');
const app = express();
const server = next({ dev: false });
server.prepare().then(() => {
app.use('/products', (req, res) => {
server.render(req, res, '/products', req.query);
});
app.use('/checkout', (req, res) => {
server.render(req, res, '/checkout', req.query);
});
app.listen(3000);
});Styling Isolation
CSS Modules
Scoped styles per component.
/* Products.module.css */
.productCard {
padding: 20px;
border: 1px solid #ddd;
}
/* Different micro-frontend can use same class name */
/* Checkout.module.css */
.productCard {
padding: 10px;
background: blue;
}Shadow DOM
Complete style isolation.
class MicroFrontend extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.container {
padding: 20px;
}
</style>
<div class="container">
<slot></slot>
</div>
`;
}
}
customElements.define('micro-frontend', MicroFrontend);CSS-in-JS
Component-scoped styles.
import styled from 'styled-components';
const StyledContainer = styled.div`
padding: 20px;
background: ${props => props.theme.background};
`;
function ProductsApp() {
return (
<ThemeProvider theme={productsTheme}>
<StyledContainer>
Products
</StyledContainer>
</ThemeProvider>
);
}Testing Strategies
Unit Testing
Test each micro-frontend independently.
// Products micro-frontend tests
import { render, screen } from '@testing-library/react';
import ProductList from './ProductList';
test('displays products', () => {
render(<ProductList products={mockProducts} />);
expect(screen.getByText('Product 1')).toBeInTheDocument();
});Integration Testing
Test interactions between micro-frontends.
// Integration test
import { render, screen, fireEvent } from '@testing-library/react';
import App from '../App';
test('adding product updates cart in header', () => {
render(<App />);
fireEvent.click(screen.getByText('Add to Cart'));
expect(screen.getByTestId('cart-badge')).toHaveTextContent('1');
});Deployment
Independent Deployments
Each micro-frontend deploys separately.
# CI/CD for checkout micro-frontend
deploy:
stage: deploy
script:
- npm run build
- aws s3 sync dist/ s3://checkout-microfrontend/
- aws cloudfront create-invalidation --distribution-id $CDN_ID --paths "/*"
only:
- checkoutCanary Releases
Gradual rollout of new versions.
// Route-based canary
function App() {
const [useNewCheckout, setUseNewCheckout] = useState(false);
useEffect(() => {
// 10% of users get new version
setUseNewCheckout(Math.random() < 0.1);
}, []);
return (
<>
{useNewCheckout ? (
<CheckoutV2 />
) : (
<CheckoutV1 />
)}
</>
);
}Best Practices
DOs
- Keep micro-frontends small and focused
- Use shared libraries for common functionality
- Implement proper error boundaries
- Monitor performance independently
- Version your APIs
- Document integration points
DON'Ts
- Create too many micro-frontends
- Duplicate code across teams
- Ignore shared dependencies
- Break backward compatibility
- Overcomplicate communication
- Skip integration testing
Performance Optimization
Code Splitting
Load micro-frontends on demand.
const CheckoutMicroFrontend = lazy(() =>
import('checkout/CheckoutApp')
);
function App() {
return (
<Suspense fallback={<Loading />}>
<CheckoutMicroFrontend />
</Suspense>
);
}Asset Optimization
Optimize assets per micro-frontend.
// Vite config
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor': ['react', 'react-dom'],
'checkout': ['@checkout/ui'],
},
},
},
},
};Monitoring
Performance Monitoring
Track each micro-frontend's performance.
// Using Web Vitals
import { getCLS, getFID, getFCP } from 'web-vitals';
getCLS(console.log);
getFID(console.log);
getFCP(console.log);
// Per micro-frontend
export function reportWebVitals(metric) {
const analytics = {
...metric,
microFrontend: 'checkout',
};
// Send to analytics
}Error Tracking
Catch and report errors.
// Error boundary
class MicroFrontendErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
console.error('Micro-frontend error:', error, errorInfo);
// Send to error tracking
}
render() {
if (this.state.hasError) {
return <ErrorFallback />;
}
return this.props.children;
}
}Conclusion
Micro-frontends enable teams to work independently and scale frontend development. However, they add complexity. Use them when you have multiple teams, need independent deployments, and have a large codebase. Start simple, monitor performance, and iterate based on feedback.