Micro-Frontend Architecture
This document explains AquaGen's micro-frontend-style architecture, built on an Nx monorepo. Module Federation is present in the codebase but currently turned off — see Module Federation — turned off below.
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 (5 apps + their -e2e projects)
│ ├── production/ # Main AquaGen app
│ ├── demo/ # Demo app
│ ├── uwms/ # AquaRecycle
│ ├── lakepulse/ # Lakepulse
│ ├── rwi/ # AquaRain
│ └── *-e2e/ # E2E test projects
├── 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 — turned off
Despite the workspace name (AquagenMfWebapp) and the @aquagen-mf-webapp/* import scope, Module Federation is turned off in every app. All apps build as plain, independent single-page apps. The Module Federation plugin is commented out in each app's rspack.config.js, and every module-federation.config.js has an empty remotes list.
For the full current-state picture, see Applications → Module Federation Status.
What this means for the rest of this document: where older text below describes runtime feature-loading, independent feature deployment, or "remotes," treat it as not active today. Shared code is combined into each app's bundle at build time via the @aquagen-mf-webapp/* path aliases, not loaded remotely.
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, uwms, lakepulse | Examples: dashboard, shared |
This repo builds five shippable apps — production, demo, uwms, lakepulse, and rwi — each a separate product. This page focuses on the shared architecture they have in common. For the apps themselves, see Applications → Overview. For the full library inventory, see Libraries & Modules.
Library Types
AquaGen uses three types of libraries:
1. Feature Libraries
Business logic and pages for specific features.
Examples: dashboard, monitoring, energy, aquaAi, 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/<feature>/src/controller/<feature>Controller.js (illustrative)
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/<feature>/src/dataSource/<feature>.js (illustrative)
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.js
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.js
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 incremental rebuilds on file changes
- 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/ — index.html, hashed JS/CSS bundles, and an assets/ folder (example layout):
dist/apps/production/
├── index.html
├── main.<hash>.js # Main bundle (hash added for production builds only)
├── styles.<hash>.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
Module Federation
Module Federation is not used — every app builds as a standard single bundle with all shared code compiled in. The federation plugins and configs are present but inert. The complete current-state description (how it is wired, and which files are dead) lives on its own page:
➡️ Applications → Module Federation Status
Performance Considerations
Performance characteristics
- Dev: Rspack gives fast startup and near-instant Hot Module Replacement on file changes.
- Production: builds are minified, tree-shaken, and hash-named; routes are lazy-loaded so the initial bundle stays smaller.
Exact build times and bundle sizes depend on the machine and the app — measure with
npx nx build production rather than relying on fixed numbers.
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:
- See the apps: Applications → Overview
- Browse the libraries: Libraries & Modules
- Learn the data flow: API Call Flow
- Learn the commands: Commands Reference
Additional Resources
Questions? See Applications → Legacy & Deprecated for what's no longer used, or Deployment & Environments for how apps ship.