Skip to main content

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

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, demoExamples: 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:

  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/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:

  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 rebuild on file changes (~100ms)
  • 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/

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

AspectCurrent (Standard)With Module Federation
BuildSingle bundleMultiple bundles
LoadingAll at startupOn-demand
DeploymentSingle deploymentIndependent
ComplexitySimpleModerate
Build timeFasterSlower (multiple builds)
RuntimeAll code loadedLazy loading
Best forRapid developmentProduction 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

  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. Explore project structure: Project Structure
  2. Learn design patterns: Design Patterns
  3. Start developing: Development Workflow
  4. Create a feature: Creating New Features

Additional Resources


Questions? See FAQ or Troubleshooting.