Micro-Frontend Architecture
This document explains the micro-frontend architecture of AquaGen, built with Nx monorepo and Module Federation patterns.
What is Micro-Frontend Architecture?
Micro-frontend architecture is an approach where a large frontend application is split into smaller, independent pieces that can be:
- Developed independently by different teams
- Deployed separately
- Scaled individually
- Tested in isolation
- Composed together at runtime
Think of it like microservices, but for the frontend.
AquaGen's Architecture Overview
Key Architectural Concepts
1. Nx Monorepo
Nx is a build framework that manages the entire codebase in a single repository.
Benefits:
- Single source of truth: All code in one place
- Shared dependencies: No version conflicts
- Efficient builds: Only rebuild what changed
- Built-in tooling: Testing, linting, and code generation
- Dependency graph: Visual representation of relationships
Directory Structure:
aquagen_web_appp/
├── apps/ # Applications (Host apps)
│ ├── production/ # Main production app
│ ├── demo/ # Demo app
│ ├── production-e2e/ # E2E tests
│ └── demo-e2e/ # E2E tests
├── libs/ # Libraries (Feature modules)
│ ├── dashboard/ # Feature: Dashboard
│ ├── monitoring/ # Feature: Monitoring
│ ├── energy/ # Feature: Energy
│ ├── shared/ # Shared utilities
│ └── components/ # Shared components
├── nx.json # Nx configuration
└── package.json # Dependencies
2. Module Federation (Currently Configured)
Module Federation allows loading features dynamically at runtime.
Current Status:
The project is configured for Module Federation (see apps/production/module-federation.config.js and rspack.config.js), but currently commented out to use standard build mode. This can be easily re-enabled when needed.
Configuration (when enabled):
// apps/production/module-federation.config.js
module.exports = {
name: 'production', // Host app name
remotes: [], // Remote modules (features)
};
Benefits when activated:
- Load features on-demand (code splitting)
- Independent deployment of features
- Faster initial page load
- Version management per feature
Why Currently Disabled:
- Simplifies build process during rapid development
- Easier debugging
- Faster builds without federation overhead
To Re-enable:
Uncomment the Module Federation plugins in apps/production/rspack.config.js:
// Currently commented lines 123-125
new NxModuleFederationPlugin({ config }, { dts: false }),
new NxModuleFederationDevServerPlugin({ config }),
3. Rspack Bundler
Rspack is a Rust-based bundler that's faster than Webpack.
Configuration: apps/production/rspack.config.js
Key features:
- Hot Module Replacement (HMR) for instant updates
- Path aliases for clean imports
- SVG handling as assets
- Security headers in dev server
- Optimized production builds
Path Aliases Example:
resolve: {
alias: {
'@aquagen-mf-webapp/shared': join(__dirname, '../../libs/shared/src'),
'@aquagen-mf-webapp/dashboard': join(__dirname, '../../libs/dashboard/src'),
// ... 20+ more aliases
}
}
Usage in code:
// Instead of relative paths:
import { api } from '../../../libs/shared/src/services/api';
// Use clean aliases:
import { api } from '@aquagen-mf-webapp/shared';
Application Structure
Apps vs Libs
| Apps | Libs |
|---|---|
| Entry points to the application | Reusable pieces of code |
| Can be served and deployed | Imported by apps and other libs |
| Contains routing and app shell | Contains features and utilities |
Examples: production, demo | Examples: dashboard, shared |
Library Types
AquaGen uses three types of libraries:
1. Feature Libraries
Business logic and pages for specific features.
Examples: dashboard, monitoring, energy, aquagpt, alerts
Structure:
libs/dashboard/
├── src/
│ ├── components/ # Feature-specific UI components
│ ├── controller/ # Business logic
│ ├── dataSource/ # API calls
│ ├── store/ # State management
│ ├── enum/ # Constants
│ ├── helper/ # Utilities
│ └── DashboardPage.jsx # Main page component
├── project.json # Nx configuration
└── tsconfig.json # TypeScript config
2. Shared Libraries
Common code used across multiple features.
Examples: shared, components, uilib
Contents:
- API clients
- Authentication services
- Common utilities
- Helper functions
- Type definitions
3. UI Libraries
Reusable presentational components.
Examples: components, uilib
Contents:
- Buttons, inputs, cards
- Layout components
- Data tables
- Charts and graphs
- Design system primitives
Dependency Rules & Constraints
To maintain a clean architecture, libraries follow strict dependency rules:
Rules:
- Shared cannot import from other libs (foundation layer)
- Components can only import from
shared - Features can import from
sharedandcomponents - Apps can import from any library
- Features cannot import from other features (prevents coupling)
Why these rules?
- Prevents circular dependencies
- Keeps code maintainable
- Makes testing easier
- Enables independent deployment (when Module Federation is active)
Design Patterns
Each feature library follows a consistent pattern:
Controller Pattern
Handles business logic and orchestration.
// libs/dashboard/src/controller/DashboardController.js
class DashboardController {
constructor(dataSource, store) {
this.dataSource = dataSource;
this.store = store;
}
async fetchDashboardData() {
const data = await this.dataSource.getData();
this.store.setDashboardData(data);
return data;
}
// More business logic...
}
DataSource Pattern
Manages API calls and data fetching.
// libs/dashboard/src/dataSource/DashboardDataSource.js
class DashboardDataSource {
constructor(apiClient) {
this.apiClient = apiClient;
}
async getData() {
return this.apiClient.get('/api/dashboard');
}
async updateData(data) {
return this.apiClient.post('/api/dashboard', data);
}
}
Store Pattern
State management using React Context.
// libs/dashboard/src/store/DashboardStore.jsx
import { createContext, useContext, useState } from 'react';
const DashboardContext = createContext();
export function DashboardProvider({ children }) {
const [dashboardData, setDashboardData] = useState(null);
const value = {
dashboardData,
setDashboardData,
};
return (
<DashboardContext.Provider value={value}>
{children}
</DashboardContext.Provider>
);
}
export const useDashboard = () => useContext(DashboardContext);
Routing Architecture
Routing is centralized in the host application:
// apps/production/src/routes/routes.jsx
import { lazy } from 'react';
const DashboardPage = lazy(() => import('@aquagen-mf-webapp/dashboard'));
const MonitoringPage = lazy(() => import('@aquagen-mf-webapp/monitoring'));
const EnergyPage = lazy(() => import('@aquagen-mf-webapp/energy'));
export const routes = [
{ path: '/dashboard', element: <DashboardPage /> },
{ path: '/monitoring', element: <MonitoringPage /> },
{ path: '/energy', element: <EnergyPage /> },
// ... more routes
];
Benefits:
- Lazy loading (load features only when needed)
- Code splitting (smaller initial bundles)
- Centralized navigation logic
Build Process
Development Build
npm start
# Runs: npx nx serve production
What happens:
- Rspack dev server starts on port 4200
- TypeScript files are transpiled
- Path aliases are resolved
- Hot Module Replacement enabled
- Source maps generated for debugging
Output:
- Served from memory (no disk writes)
- Fast rebuild on file changes (~100ms)
- Development-friendly error messages
Production Build
npm run build
# Runs: npx nx build production --skip-nx-cache
What happens:
- TypeScript compilation
- Code minification
- Tree shaking (remove unused code)
- Asset optimization
- Bundle splitting
- Hash-based file naming
Output: dist/apps/production/
dist/apps/production/
├── index.html
├── main.a1b2c3d4.js # Main bundle
├── vendor.e5f6g7h8.js # Third-party libraries
├── styles.i9j0k1l2.css # Styles
└── assets/ # Static assets
Advantages of This Architecture
For Development
- Fast onboarding: Clear structure and patterns
- Parallel development: Multiple teams on different features
- Code reuse: Shared libraries prevent duplication
- Type safety: TypeScript across all libraries
- Consistent patterns: Controller, DataSource, Store
For Operations
- Efficient builds: Nx cache and incremental builds
- Easy testing: Isolated libraries for unit tests
- Flexible deployment: Can deploy apps independently
- Monitoring: Clear boundaries for error tracking
For Business
- Faster time to market: Independent feature development
- Lower risk: Isolated changes don't affect entire app
- Scalability: Easy to add new features
- Maintainability: Clear separation of concerns
Migration Path (Module Federation)
If you want to enable Module Federation in the future:
Step 1: Uncomment Module Federation Plugins
Edit apps/production/rspack.config.js:
// Uncomment lines 4-7
const {
NxModuleFederationPlugin,
NxModuleFederationDevServerPlugin,
} = require('@nx/module-federation/rspack');
// Uncomment line 10
const config = require('./module-federation.config');
// Uncomment lines 124-125
new NxModuleFederationPlugin({ config }, { dts: false }),
new NxModuleFederationDevServerPlugin({ config }),
Step 2: Configure Remotes
Edit apps/production/module-federation.config.js:
module.exports = {
name: 'production',
remotes: [
'dashboard',
'monitoring',
'energy',
// ... add feature libraries as remotes
],
};
Step 3: Create Remote Configs
For each feature library, create a Module Federation config.
Step 4: Update Routing
Modify routing to use federated modules.
Note: This is an advanced setup and should only be done when you need independent deployments.
Comparison: Current vs Module Federation
| Aspect | Current (Standard) | With Module Federation |
|---|---|---|
| Build | Single bundle | Multiple bundles |
| Loading | All at startup | On-demand |
| Deployment | Single deployment | Independent |
| Complexity | Simple | Moderate |
| Build time | Faster | Slower (multiple builds) |
| Runtime | All code loaded | Lazy loading |
| Best for | Rapid development | Production scalability |
Performance Considerations
Current Performance Metrics
Development:
- Initial build: ~5-10 seconds
- Hot reload: <500ms
- Full rebuild: ~3-5 seconds
Production:
- Build time: ~30-60 seconds
- Bundle size: ~2-4 MB (compressed)
- Initial load: <3 seconds (depending on network)
Optimization Strategies
- Code Splitting: Lazy load routes
- Tree Shaking: Remove unused code
- Minification: Compress JavaScript/CSS
- Caching: Browser and build cache
- Compression: Gzip/Brotli on server
Next Steps
Now that you understand the architecture:
- Explore project structure: Project Structure
- Learn design patterns: Design Patterns
- Start developing: Development Workflow
- Create a feature: Creating New Features
Additional Resources
Questions? See FAQ or Troubleshooting.