π GitHub Repository: https://github.com/Dimuthu7/Micro-frontend-with-React-Vite
A comprehensive guide to building production-ready micro frontends architecture using React, TypeScript, Vite, and Module Federation. Learn how to break down monolithic frontends into independent, scalable applications.
Table of Contents
- Introduction to Micro Frontends
- Why Micro Frontends?
- Benefits of Micro Frontends Architecture
- Project Overview
- Tech Stack
- Understanding Module Federation
- Architecture Deep Dive
- Implementation Details
- Local Development Setup
- Docker Setup and Deployment
- Running the Project
- Best Practices Implemented
- Conclusion
Introduction to Micro Frontends
Micro Frontends is an architectural approach that extends the microservices pattern to the frontend. Instead of building a monolithic frontend application, you break it down into smaller, independent applications that can be developed, tested, and deployed separately.
Think of it like a restaurant: instead of one chef preparing everything, you have specialized chefs (teams) working on different dishes (features) that come together to create a complete meal (user experience).
Why Micro Frontends?
The Problem with Monolithic Frontends
As applications grow, monolithic frontends face several challenges:
- Scaling Issues: Large codebases become difficult to navigate and maintain
- Deployment Bottlenecks: One bug can block the entire release
- Team Coordination: Multiple teams working on the same codebase creates conflicts
- Technology Lock-in: Difficult to adopt new technologies or upgrade dependencies
- Performance: Large bundles slow down initial page load
Benefits of Micro Frontends Architecture
1. Team Autonomy & Scalability
- Independent Development: Teams can work in parallel without stepping on each other
- Own Technology Stack: Each team can choose frameworks that best fit their needs
- Faster Feature Delivery: Smaller codebases mean faster development cycles
- Reduced Coordination: Less need for cross-team meetings and synchronization
2. Independent Deployment
- Isolated Releases: Deploy features independently without affecting other parts
- Faster Rollouts: Release features as soon as they're ready
- Safer Rollbacks: Rollback individual features without impacting the entire app
- A/B Testing: Easier to test new features with a subset of users
3. Technology Diversity
- Framework Flexibility: Use React for one feature, Vue for another, Angular for a third
- Gradual Migration: Migrate legacy code piece by piece
- Technology Experimentation: Try new technologies without rewriting everything
- Independent Upgrades: Upgrade dependencies per micro frontend
4. Code Isolation & Maintainability
- Smaller Codebases: Easier to understand, test, and maintain
- Clear Boundaries: Well-defined interfaces prevent tight coupling
- Easier Onboarding: New developers can focus on one micro frontend
- Better Organization: Clear separation of concerns
5. Performance Optimization
- Code Splitting: Load only what's needed, when it's needed
- Independent Caching: Each micro frontend can have its own caching strategy
- Smaller Bundles: Smaller applications mean smaller bundle sizes
- Lazy Loading: Load components on-demand for better performance
6. Fault Isolation
- Error Containment: Errors in one micro frontend don't crash the entire app
- Graceful Degradation: Error boundaries can handle failures elegantly
- Better Resilience: One failing feature doesn't break the user experience
Project Overview
This project demonstrates a production-ready micro frontends architecture using a Product Catalog application. The application consists of:
- Host Application: The container/orchestrator that displays the product catalog and manages application state
- Remote Application: A micro frontend that provides detailed product information components
Project Structure
βββ host/ # Container application (Host)
β βββ src/
β β βββ components/
β β β βββ Header.tsx # Application header with cart
β β β βββ Footer.tsx # Application footer
β β β βββ ProductList.tsx # Product grid component
β β β βββ ProductListView.tsx # Product list view with header
β β β βββ ProductDetailsView.tsx # Product details view container
β β β βββ BackButton.tsx # Reusable back navigation button
β β β βββ ErrorBoundary.tsx # Error boundary for graceful error handling
β β βββ data/
β β β βββ products.ts # Product data
β β βββ types/
β β β βββ index.ts # TypeScript type definitions
β β βββ App.tsx # Main application (orchestrator)
β βββ Dockerfile
β βββ nginx.conf
βββ remote/ # Micro frontend application (Remote)
β βββ src/
β β βββ components/
β β β βββ ProductCard.tsx # Remote component (exposed via Module Federation)
β β βββ types/
β β βββ index.ts # TypeScript type definitions
β βββ Dockerfile
β βββ nginx.conf
βββ docker-compose.yml # Docker orchestration
βββ README.md
Tech Stack
Core Technologies
- React 18: Modern UI library with hooks and Suspense for async operations
- TypeScript 5: Type-safe development with compile-time error checking
- Vite 5: Fast build tool and dev server with excellent HMR (Hot Module Replacement)
- Tailwind CSS 3: Utility-first CSS framework for rapid UI development
Micro Frontends Technology
- Module Federation: Runtime module sharing and dynamic loading
- @originjs/vite-plugin-federation: Vite plugin that enables Module Federation
DevOps & Infrastructure
- Docker & Docker Compose: Containerization for consistent environments
- Nginx: Production web server with optimized configuration
- Node.js 18+: JavaScript runtime environment
Understanding Module Federation
What is Module Federation?
Module Federation is a JavaScript architecture that allows a JavaScript application to dynamically load code from another application at runtime. It was introduced by Webpack 5 and has been adapted for Vite through plugins.
Key Concepts
1. Host Application (Container)
The host application is the main application that consumes remote modules. It:
- Loads and orchestrates remote components
- Manages application state
- Handles routing and navigation
- Provides the shell/framework for the application
2. Remote Application (Micro Frontend)
The remote application exposes modules that can be consumed by the host. It:
- Exposes components, utilities, or features
- Can run independently
- Shares dependencies with the host to avoid duplication
- Maintains its own build and deployment pipeline
3. Shared Dependencies
Both host and remote can share common dependencies (like React, React-DOM) to:
- Avoid loading duplicate code
- Ensure consistent versions
- Reduce bundle sizes
- Maintain singleton instances
How Module Federation Works
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Browser (Client) β
β β
β 1. User visits Host App (localhost:5173) β
β 2. Host App loads and initializes β
β 3. User clicks on a product β
β 4. Host App requests Remote Component β
β β
β βββββββββββββββ βββββββββββββββ β
β β Host App β β Remote App β β
β β (Port 5173) ββββββββββββββββΆ β (Port 5174) β β
β β β HTTP Request β β β
β β ββββββββββββββββ β β β
β β β remoteEntry.js β β β
β β β + ProductCard β β β
β βββββββββββββββ βββββββββββββββ β
β β
β 5. Module Federation loads remote component dynamically β
β 6. Component renders in Host App context β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Architecture Deep Dive
Component Architecture
The host application follows a clean component-based architecture:
App.tsx (Orchestrator)
βββ Header (Cart display)
βββ ProductListView (Product catalog)
β βββ ProductList (Product grid)
βββ ProductDetailsView (Remote component container)
βββ RemoteProductCard (Loaded from remote app)
Data Flow
ββββββββββββββ Props βββββββββββββββ
β Host App βββββββββββββββββββββββββΆβ Remote App β
β β β β
β - State ββββββββββββββββββββββββββ - Component β
β - Handlers β Callbacks β - UI β
ββββββββββββββ βββββββββββββββ
β β
β Shared React Instance β
ββββββββββββββββββββββββββββββββββββββββ
Complete Request Flow
-
Initial Page Load
- Browser requests the host app
- Nginx serves
index.htmland host app bundle - React app initializes and renders
ProductListView - Product catalog displays
-
User Clicks Product
- Host app updates state with selected product
- Switches to
ProductDetailsView - Component lazy loads the remote component
- Module Federation makes HTTP request to remote app
-
Remote Component Loading
- Host requests
remoteEntry.jsfrom remote app - Remote app serves Module Federation manifest
- Host requests component bundle (
ProductCard.[hash].js) - Module Federation runtime loads and executes component
- Component renders in host context with shared React instance
- Host requests
-
User Interaction
- User clicks "Add to Cart" in remote component
- Callback function executes in host app
- Host app updates cart count state
- Header re-renders with new count
Implementation Details
Module Federation Configuration
Host Application (host/vite.config.ts)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'host_app',
remotes: {
remote_app: {
external: `http://${process.env.VITE_REMOTE_HOST || 'localhost'}:${process.env.VITE_REMOTE_PORT || '5174'}/assets/remoteEntry.js`,
shareScope: 'default',
},
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
build: {
modulePreload: false,
target: 'esnext',
minify: false,
cssCodeSplit: false,
},
server: {
port: Number(process.env.PORT) || 5173,
host: true,
},
});
Key Points:
-
remotes: Defines where to load remote modules from -
shared: Specifies dependencies to share (React, React-DOM) -
singleton: true: Ensures only one instance of React is loaded - Environment variables allow different URLs for different environments
Remote Application (remote/vite.config.ts)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'remote_app',
filename: 'remoteEntry.js',
exposes: {
'./ProductCard': './src/components/ProductCard',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
build: {
modulePreload: false,
target: 'esnext',
minify: false,
cssCodeSplit: false,
},
server: {
port: Number(process.env.PORT) || 5174,
host: true,
cors: true,
},
});
Key Points:
-
exposes: Defines which modules/components to expose -
filename: Name of the Module Federation manifest file -
shared: Same shared dependencies as host to avoid duplication
Loading Remote Components
ProductDetailsView Component
import React, { Suspense, lazy } from 'react';
import { ErrorBoundary } from './ErrorBoundary';
// Lazy load the remote ProductCard component
const RemoteProductCard = lazy(() => import('remote_app/ProductCard'));
interface ProductDetailsViewProps {
product: Product;
onAddToCart: () => void;
onClose: () => void;
}
export const ProductDetailsView: React.FC<ProductDetailsViewProps> = ({
product,
onAddToCart,
onClose,
}) => {
return (
<ErrorBoundary
fallback={
<div>Failed to load remote component</div>
}
>
<Suspense
fallback={
<div>Loading product details...</div>
}
>
<RemoteProductCard
product={product}
onAddToCart={onAddToCart}
/>
</Suspense>
</ErrorBoundary>
);
};
Key Features:
-
Lazy Loading: Uses React's
lazy()to dynamically import the remote component - Suspense: Provides loading state while component is being fetched
- Error Boundary: Gracefully handles errors if remote component fails to load
- Type Safety: TypeScript ensures props match the expected interface
Error Handling
The project implements a robust error boundary:
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
public render() {
if (this.state.hasError) {
return this.props.fallback || <DefaultErrorUI />;
}
return this.props.children;
}
}
This ensures that if the remote component fails to load, the application doesn't crash and shows a user-friendly error message.
Type Safety Across Applications
Both applications share the same TypeScript interfaces:
// host/src/types/index.ts
// remote/src/types/index.ts
export interface Product {
id: string;
name: string;
description: string;
price: number;
image: string;
category: string;
inStock: boolean;
}
This ensures type safety when passing data between host and remote applications.
Local Development Setup
Prerequisites
- Node.js 18+ and npm/yarn/pnpm
- Code Editor (VS Code recommended)
- Git (for cloning the repository)
Step-by-Step Setup
1. Install Dependencies
Install Remote App Dependencies:
cd remote
npm install
Install Host App Dependencies:
cd ../host
npm install
Or use the Makefile:
make install
2. Understanding the Setup
The applications need to run simultaneously because:
- The host app needs to fetch the remote component from the remote app
- Both apps need to be running for Module Federation to work
- The remote app must be accessible via HTTP
3. Running the Applications
You need two terminal windows:
Terminal 1 - Remote App (must start first):
cd remote
npm run dev
The remote app will run on http://localhost:5174
Terminal 2 - Host App:
cd host
npm run dev
The host app will run on http://localhost:5173
4. Verify the Setup
- Open your browser and navigate to
http://localhost:5173 - You should see the product catalog
- Click on any product
- The product details should load from the remote micro frontend
- Check the browser console for any errors
Environment Variables
You can customize the setup using environment variables:
Host App (.env file):
PORT=5173
VITE_REMOTE_HOST=localhost
VITE_REMOTE_PORT=5174
Remote App (.env file):
PORT=5174
Troubleshooting Local Setup
Issue: Remote component not loading
- Ensure the remote app is running before starting the host app
- Check that both apps are running on the correct ports
- Verify the remote URL in
host/vite.config.ts - Check browser console for CORS errors
Issue: CORS errors
- The remote app's Vite dev server has CORS enabled
- Ensure both apps are running
- Check that ports match the configuration
Issue: Type errors
- Run
npm run buildto check for TypeScript errors - Ensure both apps have the same type definitions
- Check that shared types are synchronized
Docker Setup and Deployment
Why Docker?
Docker provides:
- Consistent Environments: Same environment in development and production
- Easy Deployment: Package everything needed to run the application
- Isolation: Each application runs in its own container
- Orchestration: Docker Compose manages multiple containers
Docker Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Docker Network (microfrontends-network) β
β β
β ββββββββββββββββ ββββββββββββββββ β
β β host-app β β remote-app β β
β β :5173 ββββββββββββββββΆβ :5174 β β
β β (Nginx) β HTTP Request β (Nginx) β β
β β βββββββββββββββββ β β
β β β remoteEntry β β β
β ββββββββββββββββ ββββββββββββββββ β
β β
β Both containers communicate via service names β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Dockerfile Structure
Both applications use multi-stage builds:
Stage 1: Builder
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Build the application
RUN npm run build
Stage 2: Production
FROM nginx:alpine
# Install wget for health checks
RUN apk add --no-cache wget
# Copy built assets from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]
Benefits of Multi-Stage Builds:
- Smaller final image (only production files)
- Faster builds (cached layers)
- Security (no build tools in production image)
Docker Compose Configuration
services:
remote-app:
build:
context: ./remote
dockerfile: Dockerfile
container_name: micro-frontend-remote
ports:
- "5174:80"
networks:
- micro-frontend-network
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/assets/remoteEntry.js"]
interval: 10s
timeout: 5s
retries: 5
host-app:
build:
context: ./host
dockerfile: Dockerfile
args:
- VITE_REMOTE_HOST=remote-app
- VITE_REMOTE_PORT=80
container_name: micro-frontend-host
ports:
- "5173:80"
depends_on:
remote-app:
condition: service_healthy
networks:
- micro-frontend-network
networks:
micro-frontend-network:
driver: bridge
Key Features:
- Health Checks: Ensures remote app is ready before starting host app
-
Service Names: Containers communicate using service names (
remote-app) - Network Isolation: Both apps on the same Docker network
- Port Mapping: Exposes apps on host machine ports
Nginx Configuration
Why Nginx?
- Production Web Server: More performant than development servers
- CORS Headers: Required for Module Federation cross-origin requests
- SPA Routing: Handles client-side routing correctly
- Performance: Gzip compression, caching, security headers
Remote App Nginx Config
server {
listen 80;
root /usr/share/nginx/html;
# CORS headers for Module Federation
location = /assets/remoteEntry.js {
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# CORS for JavaScript files
location ~* ^/assets/.*\.js$ {
add_header Access-Control-Allow-Origin * always;
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA routing
location / {
try_files $uri $uri/ /index.html;
}
}
Key Points:
- CORS Headers: Essential for cross-origin Module Federation requests
- No Cache for remoteEntry.js: Ensures latest version is always loaded
- Cache for Assets: Optimizes performance with long-term caching
-
SPA Routing: Serves
index.htmlfor all routes
Running the Project
Option 1: Docker (Recommended for Production)
- Start Both Applications:
docker-compose up --build - Start in Detached Mode:
docker-compose up -d --build - View Logs:
docker-compose logs -f - Stop Applications:
docker-compose down
Using Makefile:
make docker-up # Start with logs
make docker-up-detached # Start in background
make docker-down # Stop containers
make docker-logs # View logs
Access Applications:
- Host App: http://localhost:5173
- Remote App: http://localhost:5174
Option 2: Local Development
Using Makefile:
make install # Install dependencies
make dev # Instructions for running both apps
Manual:
# Terminal 1
cd remote
npm run dev
# Terminal 2
cd host
npm run dev
Option 3: Production Build
Build Both Applications:
make build
Or manually:
# Build remote app first
cd remote
npm run build
# Build host app
cd ../host
npm run build
Preview Production Build:
# Terminal 1
cd remote
npm run preview
# Terminal 2
cd host
npm run preview---
Best Practices Implemented
1. Component Architecture
- Clean separation of concerns
- Focused, reusable components
- Clear component hierarchy
2. Error Handling
- Error boundaries for remote components
- Fallback UI for failed loads
- User-friendly error messages
3. Performance Optimization
- Lazy loading with React Suspense
- Code splitting per micro frontend
- Shared dependencies to reduce bundle size
- Nginx caching strategies
4. Type Safety
- Shared TypeScript types across applications
- Type-safe component props
- Compile-time error checking
5. Development Experience
- Hot Module Replacement (HMR) in development
- Clear project structure
- Environment variable configuration
- Makefile for common tasks
6. Production Readiness
- Multi-stage Docker builds
- Health checks for orchestration
- Security headers in Nginx
- Optimized build configurations
- CORS properly configured
7. Code Organization
- Shared types in both applications
- Consistent naming conventions
- Clear file structure
- Separation of concerns
Conclusion
This project demonstrates a production-ready micro frontends architecture using React, Vite, and Module Federation.
When to Use Micro Frontends
Good Fit:
- Large applications with multiple teams
- Need for independent deployments
- Different parts require different technologies
- Gradual migration from legacy systems
Not Ideal For:
- Small applications with a single team
- Tightly coupled features
- Limited DevOps resources
- Simple applications without scaling needs
Next Steps
- Add more micro frontends (e.g., user profile, checkout)
- Implement state management (Redux, Zustand)
- Add routing (React Router)
- Set up CI/CD pipelines
- Add monitoring and logging
- Implement authentication/authorization
Resources
- Module Federation Documentation
- Vite Plugin Federation
- React TypeScript Cheatsheet
- Tailwind CSS Documentation
Built with β€οΈ for learning Micro Frontends Architecture
This architecture provides a solid foundation for building scalable, maintainable frontend applications. By understanding these concepts and patterns, you can apply them to your own projects and teams.
Top comments (0)