Micro Frontends - Scaling Frontend Development
Micro Frontends: Scaling Frontend Development
As web applications grow in complexity and team sizes increase, traditional monolithic frontend architectures can become bottlenecks for development velocity and scalability. Micro frontends extend the concepts of microservices to the frontend world, allowing teams to build, test, and deploy UI components independently while creating a cohesive user experience.
Understanding Micro Frontends
What Are Micro Frontends?
Micro frontends are an architectural approach where a frontend application is decomposed into smaller, semi-independent "micro applications" that can be developed, tested, and deployed by different teams. These micro applications are then composed together to create a complete user experience.
The core idea is to apply the same principles that make microservices successful on the backend to frontend development:
- Independent development and deployment
- Team autonomy
- Technology agnosticism
- Isolation and resilience
- Scalable development
Key Principles
- Team Autonomy: Each team owns a specific part of the application from database to user interface
- Technology Independence: Teams can choose the best tools for their micro frontend
- Isolation: Micro frontends should be isolated to prevent conflicts
- Native Browser Features: Leverage built-in browser capabilities when possible
- Resilience: Failures in one micro frontend should not break the entire application
Micro Frontend Architectures
There are several approaches to implementing micro frontends, each with its own trade-offs:
1. Client-Side Composition
In this approach, micro frontends are composed in the browser using JavaScript:
// Container application
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
// Micro frontends loaded via dynamic imports or module federation
const ProductCatalog = React.lazy(() => import('productApp/ProductCatalog'));
const ShoppingCart = React.lazy(() => import('cartApp/ShoppingCart'));
const UserProfile = React.lazy(() => import('profileApp/UserProfile'));
function App() {
return (
<BrowserRouter>
<header>Common Header</header>
<React.Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route path="/products" component={ProductCatalog} />
<Route path="/cart" component={ShoppingCart} />
<Route path="/profile" component={UserProfile} />
</Switch>
</React.Suspense>
<footer>Common Footer</footer>
</BrowserRouter>
);
}
2. Server-Side Composition
With server-side composition, micro frontends are assembled on the server before being sent to the browser:
// Server-side template (using a template engine like EJS)
<!DOCTYPE html>
<html>
<head>
<title>Micro Frontend Example</title>
</head>
<body>
<header>
<%- include('header-fragment') %>
</header>
<main>
<% if (path.startsWith('/products')) { %>
<%- include('products-fragment') %>
<% } else if (path.startsWith('/cart')) { %>
<%- include('cart-fragment') %>
<% } else if (path.startsWith('/profile')) { %>
<%- include('profile-fragment') %>
<% } %>
</main>
<footer>
<%- include('footer-fragment') %>
</footer>
</body>
</html>
3. Edge-Side Composition
Edge-side composition uses edge computing platforms to assemble micro frontends at the CDN level:
// Example using Cloudflare Workers
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const url = new URL(request.url)
// Fetch the shell application
const shell = await fetch('https://shell.example.com')
let html = await shell.text()
// Determine which micro frontend to load based on the path
if (url.pathname.startsWith('/products')) {
const productContent = await fetch('https://products.example.com')
const productHtml = await productContent.text()
html = html.replace('<div id="content"></div>', productHtml)
} else if (url.pathname.startsWith('/cart')) {
const cartContent = await fetch('https://cart.example.com')
const cartHtml = await cartContent.text()
html = html.replace('<div id="content"></div>', cartHtml)
}
return new Response(html, {
headers: { 'content-type': 'text/html' },
})
}
4. Build-Time Composition
With build-time composition, micro frontends are combined during the build process:
// Using npm packages for each micro frontend
// package.json
{
"dependencies": {
"product-micro-frontend": "1.0.0",
"cart-micro-frontend": "1.0.0",
"profile-micro-frontend": "1.0.0"
}
}
// App.js
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { ProductCatalog } from 'product-micro-frontend';
import { ShoppingCart } from 'cart-micro-frontend';
import { UserProfile } from 'profile-micro-frontend';
function App() {
return (
<BrowserRouter>
<header>Common Header</header>
<Switch>
<Route path="/products" component={ProductCatalog} />
<Route path="/cart" component={ShoppingCart} />
<Route path="/profile" component={UserProfile} />
</Switch>
<footer>Common Footer</footer>
</BrowserRouter>
);
}
Implementation Techniques
1. Webpack Module Federation
Webpack 5's Module Federation allows sharing JavaScript modules between different applications at runtime:
// webpack.config.js for a micro frontend
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
// ... other webpack config
plugins: [
new ModuleFederationPlugin({
name: 'productApp',
filename: 'remoteEntry.js',
exposes: {
'./ProductCatalog': './src/components/ProductCatalog',
},
shared: ['react', 'react-dom'],
}),
],
};
// webpack.config.js for the container application
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
// ... other webpack config
plugins: [
new ModuleFederationPlugin({
name: 'container',
remotes: {
productApp: 'productApp@http://localhost:3001/remoteEntry.js',
cartApp: 'cartApp@http://localhost:3002/remoteEntry.js',
profileApp: 'profileApp@http://localhost:3003/remoteEntry.js',
},
shared: ['react', 'react-dom'],
}),
],
};
2. Web Components
Web Components provide a standards-based way to create custom, reusable UI elements:
// product-catalog.js
class ProductCatalog extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
/* Component styles */
</style>
<div class="product-catalog">
<h2>Product Catalog</h2>
<div class="products"></div>
</div>
`;
this.fetchProducts();
}
async fetchProducts() {
const response = await fetch('/api/products');
const products = await response.json();
const productsContainer = this.shadowRoot.querySelector('.products');
products.forEach(product => {
const productEl = document.createElement('div');
productEl.className = 'product';
productEl.innerHTML = `
<h3>${product.name}</h3>
<p>${product.price}</p>
<button>Add to Cart</button>
`;
productsContainer.appendChild(productEl);
});
}
}
customElements.define('product-catalog', ProductCatalog);
// In the container application's HTML
<!DOCTYPE html>
<html>
<head>
<title>Micro Frontend Example</title>
<script src="/header.js"></script>
<script src="/footer.js"></script>
</head>
<body>
<app-header></app-header>
<main id="content">
<!-- The appropriate micro frontend will be loaded here based on the route -->
</main>
<app-footer></app-footer>
<script>
// Simple router
const path = window.location.pathname;
const contentEl = document.getElementById('content');
if (path.startsWith('/products')) {
// Load the product catalog micro frontend
const script = document.createElement('script');
script.src = '/product-catalog.js';
document.head.appendChild(script);
const productCatalog = document.createElement('product-catalog');
contentEl.appendChild(productCatalog);
} else if (path.startsWith('/cart')) {
// Load the shopping cart micro frontend
// Similar approach
}
</script>
</body>
</html>
3. iframes
iframes provide strong isolation between micro frontends:
<!DOCTYPE html>
<html>
<head>
<title>Micro Frontend Example</title>
<style>
iframe {
border: none;
width: 100%;
height: 500px;
}
</style>
</head>
<body>
<header>
<!-- Common header -->
</header>
<main id="content">
<!-- The appropriate micro frontend will be loaded here based on the route -->
</main>
<footer>
<!-- Common footer -->
</footer>
<script>
// Simple router
const path = window.location.pathname;
const contentEl = document.getElementById('content');
if (path.startsWith('/products')) {
contentEl.innerHTML = '<iframe src="https://products.example.com"></iframe>';
} else if (path.startsWith('/cart')) {
contentEl.innerHTML = '<iframe src="https://cart.example.com"></iframe>';
} else if (path.startsWith('/profile')) {
contentEl.innerHTML = '<iframe src="https://profile.example.com"></iframe>';
}
</script>
</body>
</html>
4. Server-Side Includes (SSI)
Server-side includes can be used to compose micro frontends on the server:
<!DOCTYPE html>
<html>
<head>
<title>Micro Frontend Example</title>
</head>
<body>
<!--#include virtual="/header.html" -->
<!--#if expr="$REQUEST_URI = /^\/products/" -->
<!--#include virtual="/products-fragment.html" -->
<!--#elif expr="$REQUEST_URI = /^\/cart/" -->
<!--#include virtual="/cart-fragment.html" -->
<!--#elif expr="$REQUEST_URI = /^\/profile/" -->
<!--#include virtual="/profile-fragment.html" -->
<!--#endif -->
<!--#include virtual="/footer.html" -->
</body>
</html>
Communication Between Micro Frontends
Micro frontends need to communicate with each other while maintaining loose coupling:
1. Custom Events
Use browser's native custom events for communication:
// In the shopping cart micro frontend
function addToCart(product) {
// Add product to cart
// ...
// Dispatch event to notify other micro frontends
const event = new CustomEvent('cart:item-added', {
detail: { product },
bubbles: true,
});
document.dispatchEvent(event);
}
// In the header micro frontend (listening for cart updates)
document.addEventListener('cart:item-added', (event) => {
const { product } = event.detail;
updateCartIndicator();
showNotification(`Added ${product.name} to cart`);
});
2. Shared State Management
Implement a shared state management solution:
// shared-state.js
class SharedState {
constructor() {
this.state = {};
this.listeners = {};
}
getState(key) {
return this.state[key];
}
setState(key, value) {
this.state[key] = value;
if (this.listeners[key]) {
this.listeners[key].forEach(listener => listener(value));
}
}
subscribe(key, listener) {
if (!this.listeners[key]) {
this.listeners[key] = [];
}
this.listeners[key].push(listener);
return () => {
this.listeners[key] = this.listeners[key].filter(l => l !== listener);
};
}
}
// Create a singleton instance
const sharedState = new SharedState();
// Make it globally available
window.sharedState = sharedState;
export default sharedState;
// In the shopping cart micro frontend
import sharedState from './shared-state';
function addToCart(product) {
// Add product to cart
const cart = [...(sharedState.getState('cart') || []), product];
// Update shared state
sharedState.setState('cart', cart);
}
// In the header micro frontend
import sharedState from './shared-state';
function setupCartIndicator() {
const updateIndicator = (cart = []) => {
document.getElementById('cart-count').textContent = cart.length;
};
// Initialize
updateIndicator(sharedState.getState('cart'));
// Subscribe to changes
sharedState.subscribe('cart', updateIndicator);
}
3. URL/Route Based Communication
Use the URL as a communication mechanism:
// In the product catalog micro frontend
function viewProductDetails(productId) {
window.history.pushState({}, '', `/products/${productId}`);
}
// In the container application
window.addEventListener('popstate', () => {
routeToCorrectMicroFrontend(window.location.pathname);
});
function routeToCorrectMicroFrontend(path) {
// Logic to load the appropriate micro frontend based on the path
}
Styling in Micro Frontends
Consistent styling across micro frontends is crucial for a cohesive user experience:
1. Shared Design System
Implement a shared design system as a package:
// design-system package
export const Button = ({ variant = 'primary', children, ...props }) => (
<button className={`btn btn-${variant}`} {...props}>
{children}
</button>
);
export const Card = ({ title, children, ...props }) => (
<div className="card" {...props}>
{title && <div className="card-header">{title}</div>}
<div className="card-body">{children}</div>
</div>
);
// In a micro frontend
import { Button, Card } from '@company/design-system';
function ProductItem({ product }) {
return (
<Card title={product.name}>
<p>{product.description}</p>
<p>${product.price}</p>
<Button onClick={() => addToCart(product)}>Add to Cart</Button>
</Card>
);
}
2. CSS-in-JS with Scoped Styles
Use CSS-in-JS libraries to scope styles to components:
import styled from 'styled-components';
const ProductCard = styled.div`
border: 1px solid #ddd;
border-radius: 4px;
padding: 16px;
margin-bottom: 16px;
h3 {
margin-top: 0;
color: #333;
}
.price {
font-weight: bold;
color: #e53e3e;
}
button {
background-color: #3182ce;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
&:hover {
background-color: #2b6cb0;
}
}
`;
function Product({ product }) {
return (
<ProductCard>
<h3>{product.name}</h3>
<p>{product.description}</p>
<p className="price">${product.price}</p>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</ProductCard>
);
}
3. CSS Custom Properties (Variables)
Use CSS custom properties for theming:
/* shared-theme.css */
:root {
--primary-color: #3182ce;
--secondary-color: #718096;
--danger-color: #e53e3e;
--success-color: #38a169;
--text-color: #333;
--background-color: #fff;
--border-radius: 4px;
--spacing-unit: 8px;
--font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* In a micro frontend's CSS */
.product-card {
border: 1px solid #ddd;
border-radius: var(--border-radius);
padding: calc(var(--spacing-unit) * 2);
margin-bottom: calc(var(--spacing-unit) * 2);
font-family: var(--font-family);
}
.product-card h3 {
color: var(--text-color);
}
.product-card .price {
color: var(--danger-color);
}
.product-card button {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius);
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
}
Testing Micro Frontends
Testing micro frontends requires a comprehensive approach:
1. Unit Testing
Test individual components within each micro frontend:
// product-card.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import ProductCard from './ProductCard';
test('renders product information correctly', () => {
const product = {
id: '1',
name: 'Test Product',
description: 'A test product',
price: 19.99,
};
render(<ProductCard product={product} />);
expect(screen.getByText('Test Product')).toBeInTheDocument();
expect(screen.getByText('A test product')).toBeInTheDocument();
expect(screen.getByText('$19.99')).toBeInTheDocument();
});
test('calls addToCart when button is clicked', () => {
const product = {
id: '1',
name: 'Test Product',
description: 'A test product',
price: 19.99,
};
const addToCart = jest.fn();
render(<ProductCard product={product} addToCart={addToCart} />);
fireEvent.click(screen.getByText('Add to Cart'));
expect(addToCart).toHaveBeenCalledWith(product);
});
2. Integration Testing
Test how micro frontends work together:
// integration.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import App from './App';
test('adding a product to cart updates the cart count', async () => {
render(<App />);
// Navigate to products page
fireEvent.click(screen.getByText('Products'));
// Wait for products to load
const addToCartButton = await screen.findByText('Add to Cart');
// Initial cart count should be 0
expect(screen.getByTestId('cart-count')).toHaveTextContent('0');
// Add product to cart
fireEvent.click(addToCartButton);
// Cart count should be updated to 1
expect(screen.getByTestId('cart-count')).toHaveTextContent('1');
});
3. End-to-End Testing
Test the entire application flow:
// Using Cypress for E2E testing
describe('E-commerce Flow', () => {
it('allows a user to browse products, add to cart, and checkout', () => {
// Visit the homepage
cy.visit('/');
// Navigate to products
cy.contains('Products').click();
// Add a product to cart
cy.contains('Add to Cart').first().click();
// Verify cart count updated
cy.get('[data-testid="cart-count"]').should('contain', '1');
// Go to cart
cy.contains('Cart').click();
// Verify product is in cart
cy.get('.cart-item').should('have.length', 1);
// Proceed to checkout
cy.contains('Checkout').click();
// Fill in shipping information
cy.get('#name').type('John Doe');
cy.get('#email').type('john@example.com');
cy.get('#address').type('123 Main St');
// Complete order
cy.contains('Complete Order').click();
// Verify order confirmation
cy.contains('Thank you for your order').should('be.visible');
});
});
4. Contract Testing
Test the contracts between micro frontends:
// Using Pact.js for contract testing
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like } = MatchersV3;
describe('Cart API', () => {
const provider = new PactV3({
consumer: 'product-catalog',
provider: 'shopping-cart',
});
it('adds a product to the cart', async () => {
// Define the expected interaction
await provider.addInteraction({
states: [{ description: 'cart exists' }],
uponReceiving: 'a request to add a product to the cart',
withRequest: {
method: 'POST',
path: '/api/cart/items',
headers: { 'Content-Type': 'application/json' },
body: {
productId: like('123'),
quantity: like(1),
},
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
success: like(true),
cartSize: like(1),
},
},
});
// Verify the interaction
await provider.executeTest(async (mockServer) => {
const response = await fetch(`${mockServer.url}/api/cart/items`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId: '123',
quantity: 1,
}),
});
const result = await response.json();
expect(result.success).toBe(true);
expect(result.cartSize).toBe(1);
});
});
});
Implementing Micro Frontends with Next.js
Next.js can be used to implement micro frontends with its built-in features:
1. Using Next.js for Each Micro Frontend
Create separate Next.js applications for each micro frontend:
// product-catalog/next.config.js
module.exports = {
basePath: '/products',
assetPrefix: '/products',
};
// shopping-cart/next.config.js
module.exports = {
basePath: '/cart',
assetPrefix: '/cart',
};
// user-profile/next.config.js
module.exports = {
basePath: '/profile',
assetPrefix: '/profile',
};
2. Using Module Federation with Next.js
Implement Webpack Module Federation in Next.js:
// next.config.js for a micro frontend
const { NextFederationPlugin } = require('@module-federation/nextjs-mf');
module.exports = {
webpack(config, options) {
config.plugins.push(
new NextFederationPlugin({
name: 'productApp',
filename: 'static/chunks/remoteEntry.js',
exposes: {
'./ProductCatalog': './components/ProductCatalog.js',
},
shared: {
react: {
singleton: true,
requiredVersion: false,
},
'react-dom': {
singleton: true,
requiredVersion: false,
},
},
})
);
return config;
},
};
// next.config.js for the container application
const { NextFederationPlugin } = require('@module-federation/nextjs-mf');
module.exports = {
webpack(config, options) {
config.plugins.push(
new NextFederationPlugin({
name: 'container',
remotes: {
productApp: 'productApp@http://localhost:3001/_next/static/chunks/remoteEntry.js',
cartApp: 'cartApp@http://localhost:3002/_next/static/chunks/remoteEntry.js',
profileApp: 'profileApp@http://localhost:3003/_next/static/chunks/remoteEntry.js',
},
shared: {
react: {
singleton: true,
requiredVersion: false,
},
'react-dom': {
singleton: true,
requiredVersion: false,
},
},
})
);
return config;
},
};
// pages/products.js in the container application
import dynamic from 'next/dynamic';
const ProductCatalog = dynamic(() => import('productApp/ProductCatalog'), {
ssr: false, // Disable SSR for federated modules
loading: () => <p>Loading Product Catalog...</p>,
});
export default function ProductsPage() {
return (
<div>
<h1>Products</h1>
<ProductCatalog />
</div>
);
}
3. Using Next.js API Routes for Backend Communication
Implement API routes for communication between micro frontends:
// pages/api/cart/add.js in the shopping cart micro frontend
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { productId, quantity } = req.body;
// Add to cart logic
// ...
res.status(200).json({ success: true, cartSize: 1 });
}
// In the product catalog micro frontend
async function addToCart(product, quantity = 1) {
try {
const response = await fetch('/api/cart/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
productId: product.id,
quantity,
}),
});
const result = await response.json();
if (result.success) {
// Update UI or show notification
}
} catch (error) {
console.error('Error adding to cart:', error);
}
}
Challenges and Solutions
1. Consistent User Experience
Challenge: Ensuring a consistent look and feel across micro frontends.
Solution: Implement a shared design system and component library that all teams use.
// design-system/theme.js
export const theme = {
colors: {
primary: '#3182ce',
secondary: '#718096',
danger: '#e53e3e',
success: '#38a169',
text: '#333',
background: '#fff',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
},
typography: {
fontFamily: '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',
fontSize: {
small: '0.875rem',
medium: '1rem',
large: '1.25rem',
xlarge: '1.5rem',
},
},
// ... other theme properties
};
// Using the theme in a micro frontend
import { ThemeProvider } from 'styled-components';
import { theme } from '@company/design-system';
function MyMicroFrontend() {
return (
<ThemeProvider theme={theme}>
{/* Micro frontend content */}
</ThemeProvider>
);
}
2. Performance Optimization
Challenge: Multiple micro frontends can lead to performance issues.
Solution: Implement code splitting, lazy loading, and shared dependencies.
// webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
// ... other webpack config
plugins: [
new ModuleFederationPlugin({
name: 'productApp',
filename: 'remoteEntry.js',
exposes: {
'./ProductCatalog': './src/components/ProductCatalog',
},
shared: {
// Share dependencies to avoid duplicates
react: { singleton: true, eager: true },
'react-dom': { singleton: true, eager: true },
'styled-components': { singleton: true },
lodash: { singleton: true },
},
}),
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\]node_modules[\\]/,
name: 'vendors',
priority: -10,
},
},
},
},
};
3. Authentication and Authorization
Challenge: Managing user authentication across micro frontends.
Solution: Implement a centralized authentication service and use tokens.
// auth-service.js
class AuthService {
constructor() {
this.token = localStorage.getItem('auth_token');
this.user = JSON.parse(localStorage.getItem('user') || 'null');
this.listeners = [];
}
isAuthenticated() {
return !!this.token;
}
getUser() {
return this.user;
}
getToken() {
return this.token;
}
async login(credentials) {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
});
const data = await response.json();
if (data.token) {
this.token = data.token;
this.user = data.user;
localStorage.setItem('auth_token', this.token);
localStorage.setItem('user', JSON.stringify(this.user));
this.notifyListeners();
return true;
}
return false;
} catch (error) {
console.error('Login error:', error);
return false;
}
}
logout() {
this.token = null;
this.user = null;
localStorage.removeItem('auth_token');
localStorage.removeItem('user');
this.notifyListeners();
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
notifyListeners() {
this.listeners.forEach(listener => listener({
isAuthenticated: this.isAuthenticated(),
user: this.user,
}));
}
}
// Create a singleton instance
const authService = new AuthService();
// Make it globally available
window.authService = authService;
export default authService;
// Using the auth service in a micro frontend
import { useState, useEffect } from 'react';
import authService from './auth-service';
function ProfilePage() {
const [authState, setAuthState] = useState({
isAuthenticated: authService.isAuthenticated(),
user: authService.getUser(),
});
useEffect(() => {
const unsubscribe = authService.subscribe(setAuthState);
return unsubscribe;
}, []);
if (!authState.isAuthenticated) {
return <p>Please log in to view your profile.</p>;
}
return (
<div>
<h1>Profile</h1>
<p>Name: {authState.user.name}</p>
<p>Email: {authState.user.email}</p>
<button onClick={() => authService.logout()}>Log Out</button>
</div>
);
}
4. Deployment and CI/CD
Challenge: Managing deployment of multiple micro frontends.
Solution: Implement independent CI/CD pipelines with coordinated deployments.
# .github/workflows/product-catalog.yml
name: Product Catalog CI/CD
on:
push:
branches: [ main ]
paths:
- 'product-catalog/**'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: |
cd product-catalog
npm ci
- name: Build
run: |
cd product-catalog
npm run build
- name: Deploy
run: |
cd product-catalog
npm run deploy
Best Practices
1. Define Clear Boundaries
Each micro frontend should have a clear responsibility and domain boundary. Avoid overlapping responsibilities.
2. Minimize Shared State
Reduce dependencies between micro frontends by minimizing shared state. Use events for communication when possible.
3. Standardize Communication Patterns
Establish consistent patterns for how micro frontends communicate with each other.
4. Implement Comprehensive Testing
Test each micro frontend independently and test the integrated application as a whole.
5. Document Integration Points
Clearly document the APIs and events that each micro frontend exposes and consumes.
6. Monitor Performance
Implement performance monitoring to identify and address issues early.
7. Establish Governance
Create guidelines and standards for all teams to follow to ensure consistency.
Conclusion
Micro frontends offer a powerful approach to scaling frontend development across multiple teams. By applying the principles of microservices to the frontend, organizations can achieve greater team autonomy, technology flexibility, and development velocity.
However, implementing micro frontends comes with challenges, including ensuring a consistent user experience, managing performance, and coordinating deployments. By following best practices and choosing the right implementation techniques, these challenges can be overcome.
Whether you're using client-side composition with Module Federation, server-side composition, or a hybrid approach, the key is to establish clear boundaries, standardize communication patterns, and implement comprehensive testing.
As web applications continue to grow in complexity, micro frontends provide a scalable architecture that allows teams to deliver value independently while creating a cohesive experience for users.