Micro-frontends extend microservices principles to frontend development. This architecture enables teams to build, deploy, and scale frontend applications independently.
What Are Micro-Frontends?#
Micro-frontends decompose a frontend application into smaller, independent units that can be developed and deployed by separate teams.
Traditional Monolith Micro-Frontend Architecture
┌─────────────────────┐ ┌──────┬──────┬──────┐
│ │ │ Team │ Team │ Team │
│ Single Frontend │ → │ A │ B │ C │
│ Application │ │ │ │ │
│ │ │ Auth │ Shop │ Blog │
└─────────────────────┘ └──────┴──────┴──────┘
Implementation Approaches#
1. Module Federation (Webpack 5)#
Module Federation allows loading remote modules at runtime:
1// host/webpack.config.js
2const ModuleFederationPlugin = require('@module-federation/enhanced/webpack');
3
4module.exports = {
5 plugins: [
6 new ModuleFederationPlugin({
7 name: 'host',
8 remotes: {
9 shop: 'shop@http://localhost:3001/remoteEntry.js',
10 auth: 'auth@http://localhost:3002/remoteEntry.js',
11 },
12 shared: ['react', 'react-dom'],
13 }),
14 ],
15};
16
17// shop/webpack.config.js
18module.exports = {
19 plugins: [
20 new ModuleFederationPlugin({
21 name: 'shop',
22 filename: 'remoteEntry.js',
23 exposes: {
24 './ProductList': './src/components/ProductList',
25 './Cart': './src/components/Cart',
26 },
27 shared: ['react', 'react-dom'],
28 }),
29 ],
30};Use remote modules in the host:
1// host/src/App.jsx
2import React, { Suspense } from 'react';
3
4const ProductList = React.lazy(() => import('shop/ProductList'));
5const LoginForm = React.lazy(() => import('auth/LoginForm'));
6
7function App() {
8 return (
9 <div>
10 <Suspense fallback={<div>Loading...</div>}>
11 <LoginForm />
12 <ProductList />
13 </Suspense>
14 </div>
15 );
16}2. Web Components#
Framework-agnostic approach using custom elements:
1// product-card/index.js
2class ProductCard extends HTMLElement {
3 constructor() {
4 super();
5 this.attachShadow({ mode: 'open' });
6 }
7
8 static get observedAttributes() {
9 return ['product-id'];
10 }
11
12 async connectedCallback() {
13 const productId = this.getAttribute('product-id');
14 const product = await this.fetchProduct(productId);
15 this.render(product);
16 }
17
18 render(product) {
19 this.shadowRoot.innerHTML = `
20 <style>
21 .card {
22 border: 1px solid #ddd;
23 padding: 16px;
24 border-radius: 8px;
25 }
26 .price {
27 color: #2563eb;
28 font-weight: bold;
29 }
30 </style>
31 <div class="card">
32 <h3>${product.name}</h3>
33 <p class="price">$${product.price}</p>
34 <button>Add to Cart</button>
35 </div>
36 `;
37
38 this.shadowRoot.querySelector('button').addEventListener('click', () => {
39 this.dispatchEvent(new CustomEvent('add-to-cart', {
40 detail: { productId: product.id },
41 bubbles: true,
42 }));
43 });
44 }
45}
46
47customElements.define('product-card', ProductCard);3. Single-SPA Framework#
Orchestrate multiple frameworks:
1// root-config.js
2import { registerApplication, start } from 'single-spa';
3
4registerApplication({
5 name: '@company/navbar',
6 app: () => System.import('@company/navbar'),
7 activeWhen: ['/'],
8});
9
10registerApplication({
11 name: '@company/shop',
12 app: () => System.import('@company/shop'),
13 activeWhen: ['/shop'],
14});
15
16registerApplication({
17 name: '@company/blog',
18 app: () => System.import('@company/blog'),
19 activeWhen: ['/blog'],
20});
21
22start();Shared State Management#
Custom Event Bus#
1// shared/eventBus.js
2class EventBus {
3 constructor() {
4 this.events = {};
5 }
6
7 subscribe(event, callback) {
8 if (!this.events[event]) {
9 this.events[event] = [];
10 }
11 this.events[event].push(callback);
12
13 return () => {
14 this.events[event] = this.events[event].filter(cb => cb !== callback);
15 };
16 }
17
18 publish(event, data) {
19 if (!this.events[event]) return;
20 this.events[event].forEach(callback => callback(data));
21 }
22}
23
24window.__EVENT_BUS__ = window.__EVENT_BUS__ || new EventBus();
25export default window.__EVENT_BUS__;When to Use Micro-Frontends#
Good fit:
- Large teams (10+ developers)
- Multiple business domains
- Need for independent deployments
- Legacy migration scenarios
Avoid when:
- Small teams or projects
- Tight performance requirements
- Simple applications
Conclusion#
Micro-frontends provide organizational scalability at the cost of technical complexity. Start with a clear organizational need and choose the simplest integration approach that meets your requirements.