Web Development

Micro-Frontends with Module Federation Architecture Guide

Master micro frontends with module federation and webpack federation. Complete architecture guide with real-world examples for scalable applications.

· By PropTechUSA AI
16m
Read Time
3.2k
Words
7
Sections
16
Code Examples

The complexity of modern web applications has reached a tipping point. As teams grow and codebases expand, the monolithic frontend architecture that once served us well now becomes a bottleneck. Enter micro-frontends with module federation—a paradigm shift that promises to solve scalability, deployment, and team autonomy challenges while maintaining the user experience of a single, cohesive application.

The Evolution of Frontend Architecture

The journey from monolithic frontends to micro-frontends mirrors the backend's evolution from monoliths to microservices. Traditional single-page applications (SPAs) bundle everything into one deployable unit, creating dependencies that slow down development cycles and limit team independence.

Challenges with Monolithic Frontends

Monolithic frontend architectures create several pain points that become more pronounced as applications scale:

  • Deployment bottlenecks: A single change requires rebuilding and redeploying the entire application
  • Technology lock-in: The entire team must use the same framework, version, and toolchain
  • Team coordination overhead: Multiple teams working on the same codebase leads to merge conflicts and coordination complexity
  • Performance implications: Users download code for features they may never use

At PropTechUSA.ai, we've seen these challenges firsthand across various real estate technology platforms. Property management systems, listing portals, and investment platforms often start as monoliths but quickly outgrow this architecture as feature sets expand and teams grow.

The Promise of Micro-Frontends

Micro-frontends extend the microservices concept to the frontend, allowing different teams to own distinct parts of the user interface independently. This architectural pattern enables:

  • Independent deployments: Teams can deploy their micro-frontend without coordinating with other teams
  • Technology diversity: Different parts of the application can use different frameworks or versions
  • Team autonomy: Each team owns their entire stack from database to user interface
  • Incremental upgrades: Legacy parts of the application can be modernized piece by piece

Understanding Module Federation

Webpack's Module Federation represents a breakthrough in how we approach micro-frontends. Unlike previous solutions that relied on iframe sandboxing or server-side composition, module federation enables true runtime composition of JavaScript modules across different builds.

Core Concepts of Module Federation

Module federation introduces several key concepts that form the foundation of this architecture:

Host Applications serve as the main entry point and orchestrate the loading of remote modules. The host defines the overall application shell and routing structure. Remote Applications expose specific modules or components that can be consumed by hosts or other remotes. These are self-contained applications that can also run independently. Exposed Modules are the specific pieces of functionality that a remote makes available to other applications. These could be entire pages, components, or utility functions. Shared Dependencies allow multiple applications to share common libraries like React, reducing bundle size and ensuring consistency.

Technical Architecture Overview

The module federation plugin creates a special entry point that handles the dynamic loading and sharing of modules. When a host application requests a remote module, webpack's runtime performs the following steps:

  • Module Resolution: The host identifies the remote application and requested module
  • Dynamic Loading: The remote application's code is fetched at runtime
  • Dependency Sharing: Shared dependencies are negotiated and resolved
  • Module Instantiation: The remote module is instantiated within the host's context

This process happens transparently to the developer, making remote modules feel like local imports.

Implementation Deep Dive

Let's examine how to implement micro-frontends using module federation with practical examples that demonstrate real-world scenarios.

Setting Up the Host Application

The host application serves as the main container and typically handles routing, authentication, and overall application state. Here's how to configure a host using webpack's ModuleFederationPlugin:

typescript
// webpack.config.js class="kw">for host application class="kw">const ModuleFederationPlugin = require('@module-federation/webpack');

module.exports = {

mode: 'development',

devServer: {

port: 3000,

},

plugins: [

new ModuleFederationPlugin({

name: 'host',

remotes: {

propertySearch: 'propertySearch@http://localhost:3001/remoteEntry.js',

userDashboard: 'userDashboard@http://localhost:3002/remoteEntry.js',

analytics: 'analytics@http://localhost:3003/remoteEntry.js'

},

shared: {

react: { singleton: true },

'react-dom': { singleton: true },

'react-router-dom': { singleton: true }

}

})

]

};

The host application can then dynamically import and use remote modules:

typescript
// Host application component import React, { Suspense } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; class="kw">const PropertySearch = React.lazy(() => import('propertySearch/SearchApp')); class="kw">const UserDashboard = React.lazy(() => import('userDashboard/DashboardApp')); class="kw">const Analytics = React.lazy(() => import('analytics/AnalyticsApp')); class="kw">function App() {

class="kw">return (

<BrowserRouter>

<div className="app-container">

<Navigation />

<Suspense fallback={<div>Loading...</div>}>

<Routes>

<Route path="/search/*" element={<PropertySearch />} />

<Route path="/dashboard/*" element={<UserDashboard />} />

<Route path="/analytics/*" element={<Analytics />} />

</Routes>

</Suspense>

</div>

</BrowserRouter>

);

}

Creating Remote Applications

Each remote application needs to expose modules through the ModuleFederationPlugin and can optionally serve as a standalone application:

typescript
// webpack.config.js class="kw">for property search remote class="kw">const ModuleFederationPlugin = require(&#039;@module-federation/webpack&#039;);

module.exports = {

mode: &#039;development&#039;,

devServer: {

port: 3001,

},

plugins: [

new ModuleFederationPlugin({

name: &#039;propertySearch&#039;,

filename: &#039;remoteEntry.js&#039;,

exposes: {

&#039;./SearchApp&#039;: &#039;./src/SearchApp&#039;,

&#039;./PropertyCard&#039;: &#039;./src/components/PropertyCard&#039;,

&#039;./SearchFilters&#039;: &#039;./src/components/SearchFilters&#039;

},

shared: {

react: { singleton: true },

&#039;react-dom&#039;: { singleton: true }

}

})

]

};

The exposed SearchApp component handles its own routing and state management:

typescript
// SearchApp.tsx - Remote application import React from &#039;react&#039;; import { Routes, Route } from &#039;react-router-dom&#039;; import SearchResults from &#039;./pages/SearchResults&#039;; import PropertyDetails from &#039;./pages/PropertyDetails&#039;; import { SearchProvider } from &#039;./context/SearchContext&#039;; class="kw">const SearchApp: React.FC = () => {

class="kw">return (

<SearchProvider>

<div className="search-app">

<Routes>

<Route path="/" element={<SearchResults />} />

<Route path="/property/:id" element={<PropertyDetails />} />

</Routes>

</div>

</SearchProvider>

);

};

export default SearchApp;

Managing Shared State and Communication

One of the biggest challenges in micro-frontend architecture is managing communication between different applications. Here's an approach using a shared event bus:

typescript
// shared/EventBus.ts class EventBus {

private events: { [key: string]: Function[] } = {};

emit(event: string, data?: any) {

class="kw">if (this.events[event]) {

this.events[event].forEach(callback => callback(data));

}

}

on(event: string, callback: Function) {

class="kw">if (!this.events[event]) {

this.events[event] = [];

}

this.events[event].push(callback);

}

off(event: string, callback: Function) {

class="kw">if (this.events[event]) {

this.events[event] = this.events[event].filter(cb => cb !== callback);

}

}

}

export class="kw">const eventBus = new EventBus();

This event bus can be shared across all micro-frontends to enable communication:

typescript
// In the property search remote import { eventBus } from &#039;shared/EventBus&#039;; class="kw">const handlePropertySelect = (property: Property) => {

eventBus.emit(&#039;property:selected&#039;, {

id: property.id,

price: property.price,

location: property.location

});

};

// In the analytics remote import { eventBus } from &#039;shared/EventBus&#039;; useEffect(() => {

class="kw">const handlePropertySelection = (propertyData: any) => {

trackEvent(&#039;property_viewed&#039;, propertyData);

};

eventBus.on(&#039;property:selected&#039;, handlePropertySelection);

class="kw">return () => {

eventBus.off(&#039;property:selected&#039;, handlePropertySelection);

};

}, []);

Best Practices and Production Considerations

Implementing micro-frontends with module federation requires careful consideration of several factors to ensure a robust, maintainable architecture.

Dependency Management Strategy

Shared dependencies are crucial for performance and consistency but require careful management:

typescript
// webpack.config.js - Advanced shared configuration

shared: {

react: {

singleton: true,

requiredVersion: &#039;^18.0.0&#039;,

eager: true

},

&#039;react-dom&#039;: {

singleton: true,

requiredVersion: &#039;^18.0.0&#039;

},

&#039;@mui/material&#039;: {

singleton: true,

requiredVersion: &#039;^5.0.0&#039;

},

&#039;date-fns&#039;: {

singleton: false, // Allow multiple versions

requiredVersion: false

}

}

💡
Pro Tip
Use eager: true for critical dependencies that should be loaded immediately. This prevents loading delays but increases initial bundle size.

Error Handling and Fallbacks

Robust error handling is essential when dealing with remote modules that might fail to load:

typescript
// ErrorBoundary class="kw">for remote modules import React, { Component, ErrorInfo, ReactNode } from &#039;react&#039;; interface Props {

children: ReactNode;

fallback: ReactNode;

moduleName: string;

}

interface State {

hasError: boolean;

}

class RemoteErrorBoundary extends Component<Props, State> {

public state: State = {

hasError: false

};

public static getDerivedStateFromError(_: Error): State {

class="kw">return { hasError: true };

}

public componentDidCatch(error: Error, errorInfo: ErrorInfo) {

console.error(Error loading remote module ${this.props.moduleName}:, error, errorInfo);

// Send error to monitoring service

this.reportError(error, this.props.moduleName);

}

private reportError(error: Error, moduleName: string) {

// Integration with error monitoring

class="kw">if (window.analytics) {

window.analytics.track(&#039;remote_module_error&#039;, {

moduleName,

error: error.message,

stack: error.stack

});

}

}

public render() {

class="kw">if (this.state.hasError) {

class="kw">return this.props.fallback;

}

class="kw">return this.props.children;

}

}

// Usage in host application

<RemoteErrorBoundary

moduleName="propertySearch"

fallback={<div>Property search is temporarily unavailable</div>}

>

<Suspense fallback={<LoadingSpinner />}>

<PropertySearch />

</Suspense>

</RemoteErrorBoundary>

Performance Optimization

Module federation can impact performance if not properly optimized. Consider these strategies:

Lazy Loading with Preloading:
typescript
// Preload critical remotes class="kw">const preloadRemote = (remoteName: string) => {

class="kw">const script = document.createElement(&#039;link&#039;);

script.rel = &#039;modulepreload&#039;;

script.href = http://localhost:3001/remoteEntry.js;

document.head.appendChild(script);

};

// Preload on user interaction class="kw">const handleNavigationHover = () => {

preloadRemote(&#039;propertySearch&#039;);

};

Bundle Analysis and Optimization:
typescript
// webpack.config.js - Production optimizations

optimization: {

splitChunks: {

chunks: &#039;class="kw">async&#039;,

cacheGroups: {

vendor: {

test: /[\\\/]node_modules[\\\/]/,

name: &#039;vendors&#039;,

chunks: &#039;all&#039;,

},

},

},

},

Security Considerations

Security in micro-frontend architectures requires attention to several areas:

  • Content Security Policy (CSP): Configure CSP headers to allow loading of remote modules from trusted domains
  • Runtime integrity checks: Implement checksum verification for remote modules in production
  • Authentication propagation: Ensure authentication tokens are securely shared across micro-frontends
typescript
// Secure remote loading with integrity checks class="kw">const loadRemoteWithIntegrity = class="kw">async (remoteName: string, expectedHash: string) => {

class="kw">const response = class="kw">await fetch(/api/remotes/${remoteName}/info);

class="kw">const { url, hash } = class="kw">await response.json();

class="kw">if (hash !== expectedHash) {

throw new Error(Integrity check failed class="kw">for remote ${remoteName});

}

class="kw">return import(url);

};

⚠️
Warning
Always validate remote module integrity in production environments to prevent code injection attacks.

Advanced Patterns and Real-World Applications

As micro-frontend architectures mature, several advanced patterns have emerged that address complex real-world scenarios.

Dynamic Remote Discovery

In large organizations, the number of micro-frontends can grow significantly. Dynamic discovery allows for more flexible architectures:

typescript
// Dynamic remote configuration interface RemoteConfig {

name: string;

url: string;

scope: string;

module: string;

version: string;

}

class RemoteManager {

private remotes: Map<string, RemoteConfig> = new Map();

class="kw">async discoverRemotes(): Promise<void> {

class="kw">const response = class="kw">await fetch(&#039;/api/micro-frontends/discover&#039;);

class="kw">const configs: RemoteConfig[] = class="kw">await response.json();

configs.forEach(config => {

this.remotes.set(config.name, config);

});

}

class="kw">async loadRemote(name: string): Promise<any> {

class="kw">const config = this.remotes.get(name);

class="kw">if (!config) {

throw new Error(Remote ${name} not found);

}

// Dynamically add remote to webpack

class="kw">const container = window[config.scope];

class="kw">await container.init(__webpack_share_scopes__.default);

class="kw">const factory = class="kw">await container.get(config.module);

class="kw">return factory();

}

}

Cross-Framework Integration

Module federation supports different frameworks within the same application:

typescript
// React wrapper class="kw">for Vue micro-frontend import React, { useEffect, useRef } from &#039;react&#039;; import { createApp } from &#039;vue&#039;; class="kw">const VueMicroFrontend: React.FC<{ component: any; props: any }> = ({

component,

props

}) => {

class="kw">const ref = useRef<HTMLDivElement>(null);

class="kw">const vueAppRef = useRef<any>(null);

useEffect(() => {

class="kw">if (ref.current) {

vueAppRef.current = createApp(component, props);

vueAppRef.current.mount(ref.current);

}

class="kw">return () => {

class="kw">if (vueAppRef.current) {

vueAppRef.current.unmount();

}

};

}, [component, props]);

class="kw">return <div ref={ref} />;

};

At PropTechUSA.ai, we've successfully implemented similar cross-framework patterns where legacy jQuery components are wrapped and integrated into modern React applications, allowing for gradual modernization of large real estate platforms.

Testing Strategies

Testing micro-frontends requires a multi-layered approach:

typescript
// Integration test class="kw">for micro-frontend composition import { render, screen, waitFor } from &#039;@testing-library/react&#039;; import { setupServer } from &#039;msw/node&#039;; import { rest } from &#039;msw&#039;; class="kw">const server = setupServer(

rest.get(&#039;/remoteEntry.js&#039;, (req, res, ctx) => {

class="kw">return res(ctx.text(mockRemoteEntry));

})

);

describe(&#039;Micro-frontend Integration&#039;, () => {

beforeAll(() => server.listen());

afterEach(() => server.resetHandlers());

afterAll(() => server.close());

test(&#039;should load and render remote component&#039;, class="kw">async () => {

render(<App />);

class="kw">await waitFor(() => {

expect(screen.getByTestId(&#039;property-search&#039;)).toBeInTheDocument();

});

});

test(&#039;should handle remote component failure gracefully&#039;, class="kw">async () => {

server.use(

rest.get(&#039;/remoteEntry.js&#039;, (req, res, ctx) => {

class="kw">return res(ctx.status(500));

})

);

render(<App />);

class="kw">await waitFor(() => {

expect(screen.getByText(&#039;Property search is temporarily unavailable&#039;))

.toBeInTheDocument();

});

});

});

Deployment and DevOps Considerations

Successful micro-frontend deployments require careful orchestration of multiple applications and their dependencies.

CI/CD Pipeline Architecture

Each micro-frontend should have its own deployment pipeline, but coordination is essential:

yaml
# .github/workflows/deploy-remote.yml

name: Deploy Property Search Remote

on:

push:

branches: [main]

jobs:

deploy:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v3

- name: Build and Test

run: |

npm ci

npm run test

npm run build

- name: Deploy to CDN

run: |

aws s3 sync dist/ s3://micro-frontends-cdn/property-search/

aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_ID --paths "/property-search/*"

- name: Update Service Registry

run: |

curl -X POST $SERVICE_REGISTRY_URL/remotes \

-H "Content-Type: application/json" \

-d &#039;{

"name": "propertySearch",

"version": "${{ github.sha }}",

"url": "https://cdn.example.com/property-search/remoteEntry.js"

}&#039;

Monitoring and Observability

Micro-frontends require comprehensive monitoring across multiple dimensions:

typescript
// Monitoring integration interface MicroFrontendMetrics {

loadTime: number;

errorRate: number;

renderTime: number;

bundleSize: number;

}

class MicroFrontendObserver {

private metrics: Map<string, MicroFrontendMetrics> = new Map();

trackRemoteLoad(remoteName: string, startTime: number) {

class="kw">const loadTime = performance.now() - startTime;

// Send metrics to observability platform

this.sendMetric(&#039;micro_frontend_load_time&#039;, loadTime, {

remote: remoteName,

version: this.getRemoteVersion(remoteName)

});

}

trackError(remoteName: string, error: Error) {

this.sendMetric(&#039;micro_frontend_error&#039;, 1, {

remote: remoteName,

errorType: error.name,

errorMessage: error.message

});

}

private sendMetric(name: string, value: number, tags: Record<string, string>) {

// Integration with DataDog, New Relic, etc.

class="kw">if (window.analytics) {

window.analytics.track(name, { value, ...tags });

}

}

}

Future of Micro-Frontends and Module Federation

The micro-frontend ecosystem continues to evolve rapidly, with several emerging trends shaping its future:

Enhanced Developer Experience: Tools like @module-federation/nextjs-mf and Nx are making micro-frontend development more accessible and integrated with existing workflows. Edge Computing Integration: Module federation is being optimized for edge deployments, allowing for geographically distributed micro-frontends that improve performance for global applications. Server-Side Rendering (SSR) Support: Advanced SSR capabilities are being developed to support micro-frontends in server-rendered applications, addressing SEO and performance concerns.

The real estate technology sector, where PropTechUSA.ai operates, particularly benefits from these advances. Property platforms often need to integrate multiple specialized services—listing management, mortgage calculators, virtual tours, and analytics—making them ideal candidates for micro-frontend architectures.

Micro-frontends with module federation represent a significant step forward in frontend architecture, offering solutions to many of the scalability and maintainability challenges facing modern web applications. While the implementation requires careful planning and adherence to best practices, the benefits of team autonomy, independent deployments, and technological diversity make it a compelling choice for complex applications.

As you consider implementing micro-frontends in your architecture, start small with a pilot project, invest in proper tooling and monitoring, and prioritize developer experience. The patterns and practices outlined in this guide provide a solid foundation, but remember that every application has unique requirements that may necessitate adaptations to these approaches.

Ready to modernize your frontend architecture? Start by identifying the boundaries in your current application and consider how micro-frontends could improve your team's velocity and deployment independence.

Need This Built?
We build production-grade systems with the exact tech covered in this article.
Start Your Project
PT
PropTechUSA.ai Engineering
Technical Content
Deep technical content from the team building production systems with Cloudflare Workers, AI APIs, and modern web infrastructure.