Guía de arquitectura de micro frontends
Guía práctica de micro frontends: dividir un frontend monolítico en piezas desplegables por separado con Module Federation y diseño compartido.

Los micro frontends aplican el principio de los microservicios al frontend: dividir una interfaz monolítica de gran tamaño en aplicaciones más pequeñas que se desarrollan, prueban y despliegan de forma independiente, y que se combinan para formar una única experiencia de usuario. Cada equipo es dueño de una porción vertical del producto — desde el componente de la interfaz hasta la llamada a la API — y puede publicar cambios sin coordinar despliegues con el resto de los equipos.
El costo a cambio es una complejidad real. Se está introduciendo composición en tiempo de ejecución, gestión de estado compartido entre aplicaciones y una posible inconsistencia en la experiencia de usuario. Los micro frontends resuelven problemas de escalado organizacional, no problemas técnicos. Si tienes un solo equipo de frontend, no los necesitas.
Estrategias de composición
Existen varias formas de componer micro frontends. La elección correcta depende de tus requisitos de rendimiento, la estructura del equipo y el grado de acoplamiento que necesiten los distintos componentes de tu aplicación.
// Strategy 1: Route-based composition
// Each route loads a different application entirely
// Simplest approach — applications are fully isolated
interface RouteConfig {
path: string;
appName: string;
appUrl: string; // Where the micro frontend is hosted
activeWhen: (location: Location) => boolean;
}
const routes: RouteConfig[] = [
{
path: '/dashboard',
appName: 'dashboard-app',
appUrl: 'https://dashboard.cdn.example.com/main.js',
activeWhen: (loc) => loc.pathname.startsWith('/dashboard'),
},
{
path: '/settings',
appName: 'settings-app',
appUrl: 'https://settings.cdn.example.com/main.js',
activeWhen: (loc) => loc.pathname.startsWith('/settings'),
},
{
path: '/billing',
appName: 'billing-app',
appUrl: 'https://billing.cdn.example.com/main.js',
activeWhen: (loc) => loc.pathname.startsWith('/billing'),
},
];
// The shell app loads and mounts the right micro frontend
// based on the current route — like a frontend load balancer// Strategy 2: Component-based composition with Module Federation
// Multiple applications can share components at runtime
// Webpack 5 Module Federation enables this natively
// webpack.config.js for the host (shell) application
const hostConfig = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
// Load components from other micro frontends at runtime
dashboardApp: 'dashboard@https://dashboard.cdn.example.com/remoteEntry.js',
billingApp: 'billing@https://billing.cdn.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
// webpack.config.js for the dashboard micro frontend
const dashboardConfig = {
plugins: [
new ModuleFederationPlugin({
name: 'dashboard',
filename: 'remoteEntry.js',
exposes: {
'./DashboardWidget': './src/components/DashboardWidget',
'./MetricsPanel': './src/components/MetricsPanel',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};La aplicación shell
La aplicación shell (o host) proporciona el layout compartido — navegación, autenticación, manejo global de errores — y orquesta la carga de los micro frontends.
// Shell application — provides layout and orchestration
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Lazy-load remote micro frontends
const DashboardApp = lazy(() => import('dashboardApp/DashboardWidget'));
const BillingApp = lazy(() => import('billingApp/BillingPage'));
const SettingsApp = lazy(() => import('settingsApp/SettingsPage'));
function Shell() {
return (
<BrowserRouter>
<div className="app-layout">
<SharedNavigation />
<main>
<Suspense fallback={<LoadingSkeleton />}>
<ErrorBoundary fallback={<MicroFrontendError />}>
<Routes>
<Route path="/dashboard/*" element={<DashboardApp />} />
<Route path="/billing/*" element={<BillingApp />} />
<Route path="/settings/*" element={<SettingsApp />} />
</Routes>
</ErrorBoundary>
</Suspense>
</main>
</div>
</BrowserRouter>
);
}
// Error boundary prevents one crashed micro frontend from taking down the whole app
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error) {
console.error('Micro frontend error:', error);
// Report to monitoring — which micro frontend failed?
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}// ❌ No error isolation — one micro frontend crash kills everything
function ShellNoIsolation() {
return (
<Routes>
<Route path="/dashboard/*" element={<DashboardApp />} />
<Route path="/billing/*" element={<BillingApp />} />
{/* If BillingApp throws, the entire page goes white */}
</Routes>
);
}
// ✅ Each micro frontend wrapped in its own error boundary
function ShellWithIsolation() {
return (
<Routes>
<Route path="/dashboard/*" element={
<ErrorBoundary fallback={<p>Dashboard unavailable</p>}>
<Suspense fallback={<LoadingSkeleton />}>
<DashboardApp />
</Suspense>
</ErrorBoundary>
} />
<Route path="/billing/*" element={
<ErrorBoundary fallback={<p>Billing unavailable</p>}>
<Suspense fallback={<LoadingSkeleton />}>
<BillingApp />
</Suspense>
</ErrorBoundary>
} />
</Routes>
);
}Comunicación entre aplicaciones
Los micro frontends necesitan comunicarse entre sí: la navegación necesita conocer el estado de autenticación del usuario y la página de facturación necesita saber qué equipo está seleccionado. La restricción clave es que la comunicación debe mantener un acoplamiento débil. Las importaciones directas entre micro frontends van en contra de ese propósito.
// Custom events for loose coupling between micro frontends
// Any micro frontend can emit or listen without importing another
interface AppEvent {
type: string;
payload: unknown;
source: string; // Which micro frontend emitted this
}
class EventBus {
private listeners = new Map<string, Set<(event: AppEvent) => void>>();
on(eventType: string, handler: (event: AppEvent) => void): () => void {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, new Set());
}
this.listeners.get(eventType)!.add(handler);
// Return unsubscribe function
return () => this.listeners.get(eventType)?.delete(handler);
}
emit(event: AppEvent): void {
const handlers = this.listeners.get(event.type) ?? [];
for (const handler of handlers) {
try {
handler(event);
} catch (error) {
console.error(`Event handler error for "${event.type}":`, error);
}
}
}
}
// Singleton event bus shared via window (or a shared module)
const eventBus = (window as any).__EVENT_BUS__ ??= new EventBus();
// Dashboard micro frontend emits team selection
eventBus.emit({
type: 'team:selected',
payload: { teamId: 'team-123', teamName: 'Platform' },
source: 'dashboard-app',
});
// Billing micro frontend listens for team changes
eventBus.on('team:selected', (event) => {
const { teamId } = event.payload as { teamId: string };
loadBillingForTeam(teamId);
});Sistema de diseño compartido
La consistencia visual entre micro frontends requiere un sistema de diseño compartido: una biblioteca de componentes, tokens y estilos que utilizan todas las aplicaciones.
// @company/design-system — published as an npm package
// Each micro frontend imports from this shared library
// Versioned and published independently
// Teams upgrade on their own schedule (within a compatibility window)
// design-system/src/Button.tsx
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size: 'sm' | 'md' | 'lg';
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}
export function Button({ variant, size, children, ...props }: ButtonProps) {
return (
<button
className={`ds-btn ds-btn--${variant} ds-btn--${size}`}
{...props}
>
{children}
</button>
);
}
// design-system/src/tokens.ts — shared design tokens
export const tokens = {
colors: {
primary: '#2563eb',
danger: '#dc2626',
neutral: {
50: '#f8fafc',
900: '#0f172a',
},
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
},
borderRadius: {
sm: '0.25rem',
md: '0.5rem',
full: '9999px',
},
} as const;Cuándo vale la pena usar micro frontends
Los micro frontends añaden complejidad. Los beneficios provienen de la independencia organizacional: los equipos pueden publicar sin esperarse unos a otros. Si tu organización tiene 3 o más equipos de frontend, la coordinación de despliegues es un cuello de botella, y las distintas partes de la aplicación tienen ritmos de lanzamiento diferentes, vale la pena evaluar los micro frontends.
Si tienes un solo equipo, un monolito bien estructurado con code splitting es más simple e igual de efectivo.
Puntos clave
- Los micro frontends resuelven problemas organizacionales, no técnicos — permiten el despliegue independiente de los equipos, no una mejor arquitectura de frontend
- Elige la estrategia de composición según las necesidades de acoplamiento — basada en rutas para un aislamiento total, Module Federation para componentes compartidos
- La aplicación shell proporciona la infraestructura compartida — navegación, autenticación, manejo de errores y orquestación de los micro frontends
- Usa un error boundary por cada micro frontend — que una aplicación falle no debería tumbar toda la página
- Comunícate mediante eventos, no mediante importaciones — el acoplamiento débil a través de un bus de eventos preserva la independencia de los equipos
- Comparte un sistema de diseño, no código — una biblioteca de componentes versionada garantiza la consistencia visual sin acoplamiento en tiempo de ejecución


