VOOZH about

URL: https://dev.to/mahdi_benrhouma_fe1c6005/micro-frontends-the-complete-architecture-guide-for-2026-41kf

⇱ Micro-Frontends: The Complete Architecture Guide for 2026 - DEV Community


Micro-Frontends: The Complete Architecture Guide for 2026

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
FROMnode:18-alpineASbuilder

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.

Related Articles

Explore more articles in our API Development series:


Originally published at https://iloveblogs.blog