DEV Community

Cover image for Micro-Frontends: The Complete Architecture Guide for 2026
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on • Originally published at iloveblogs.blog

Micro-Frontends: The Complete Architecture Guide for 2026

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 },
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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());
  },
});
Enter fullscreen mode Exit fullscreen mode

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;
  }
`;
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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');
  });
});
Enter fullscreen mode Exit fullscreen mode

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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;"]
Enter fullscreen mode Exit fullscreen mode
## 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  }))
);
Enter fullscreen mode Exit fullscreen mode

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;
          });
        });
      })
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

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',
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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
    }));
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)