Skip to main content

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

Module Federation is not used

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

AppsLibs
Entry points to the applicationReusable pieces of code
Can be served and deployedImported by apps and other libs
Contains routing and app shellContains features and utilities
Examples: production, uwms, lakepulseExamples: dashboard, shared
There are five apps, not two

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:

  1. Shared cannot import from other libs (foundation layer)
  2. Components can only import from shared
  3. Features can import from shared and components
  4. Apps can import from any library
  5. 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:

  1. Rspack dev server starts on port 4200
  2. TypeScript files are transpiled
  3. Path aliases are resolved
  4. Hot Module Replacement enabled
  5. 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:

  1. TypeScript compilation
  2. Code minification
  3. Tree shaking (remove unused code)
  4. Asset optimization
  5. Bundle splitting
  6. 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

  1. Code Splitting: Lazy load routes
  2. Tree Shaking: Remove unused code
  3. Minification: Compress JavaScript/CSS
  4. Caching: Browser and build cache
  5. Compression: Gzip/Brotli on server

Next Steps

Now that you understand the architecture:

  1. See the apps: Applications → Overview
  2. Browse the libraries: Libraries & Modules
  3. Learn the data flow: API Call Flow
  4. 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.