Architecting Micro-Frontends with Nx, Rspack, and Module Federation

A practical guide to building a micro-frontend platform in an Nx monorepo using Rspack and Module Federation, with React as the UI layer.

architecturemicro-frontendsmodule-federationnxrspackreact

What This Guide Covers

This is a field guide to designing a micro-frontend (MFE) platform using:

  • Nx monorepo for orchestration and dependency boundaries
  • Rspack for fast bundling and dev servers
  • Module Federation for runtime composition
  • React for UI and state boundaries

High-Level Architecture

We use a single Host application that provides the shell (routing, auth context, layout) and loads four domain MFEs at runtime:

  • Telemetry MFE
  • Orchestration MFE
  • Admin MFE
  • Compliance MFE
graph TD
  C[CDN Edge Cache] --> H[Host App: Auth, Routing, Layout]
  subgraph MFEs
    direction TB
    T[Telemetry MFE]
    O[Orchestration MFE]
    A[Admin MFE]
    K[Compliance MFE]
  end
  H -->|Module Federation| MFEs
  subgraph Shared
    direction TB
    DS[Prism UI and Tokens]
    Auth[Identity Utils]
    Types[Shared Contracts]
  end
  H -->|Shared Dependencies| Shared
  MFEs -->|Shared Dependencies| Shared

CLI Commands for Scaffolding

This command creates the workspace

npx create-nx-workspace@latest nx-mfe-demo --preset=apps

This command creates the host application and the remotes.

npx nx g @nx/react:host \
  --directory=apps/host \
  --name=host \
  --remotes=telemetry,orchestration,admin,compliance \
  --style=css \
  --e2eTestRunner=none \
  --no-interactive

This command creates libraries like identity-utils, prism-ui and shared-contracts.

npx nx g @nx/react:library \
  --directory=libs/identity-utils \
  --name=identity-utils \
  --bundler=none \
  --no-interactive

View the dependency graph

npx nx graph

Composition Approach

Client-side composition via Module Federation. The Host loads MFEs at runtime from each remote’s remoteEntry.js, which enables independent deployments with a single URL experience.

This avoids iframe drawbacks (UX inconsistencies, SEO limits, awkward communication) while keeping build-time coupling minimal. If you later need SSR for the host (edge rendering or first-paint gains), you can add it without changing the MFE contract. For dashboard-style apps, client-side is usually enough.

Routing and Navigation

  • The Host owns top-level routes and global navigation
  • Each MFE owns internal routes under a stable base path
  • Deep links are supported (example: /telemetry/*)
  • The Host wraps MFEs with auth and error boundaries

Entry points:

  • Host router and layout: apps/host/src/app/app.tsx
  • Host bootstrap: apps/host/src/bootstrap.tsx
// apps/host/src/app/app.tsx
const App: React.FC = () => {
  return (
    <BrowserRouter>
      <IdentityProvider>
        <ShellLayout>
          <Routes>
            <Route path="/telemetry/*" element={<TelemetryRemote />} />
            <Route path="/orchestration/*" element={<OrchestrationRemote />} />
            <Route path="/admin/*" element={<AdminRemote />} />
            <Route path="/compliance/*" element={<ComplianceRemote />} />
          </Routes>
        </ShellLayout>
      </IdentityProvider>
    </BrowserRouter>
  );
};

Each MFE accepts a basePath and handles its internal routes:

// apps/telemetry/src/app/App.tsx
interface TelemetryAppProps {
  basePath: string; // '/telemetry'
}

const TelemetryApp: React.FC<TelemetryAppProps> = ({ basePath }) => {
  return (
    <Routes>
      <Route path={`${basePath}/overview`} element={<Overview />} />
      <Route path={`${basePath}/alerts`} element={<Alerts />} />
      <Route path={`${basePath}/settings`} element={<Settings />} />
    </Routes>
  );
};

Design System Sharing and Drift Control

Shared UI components and design tokens live in libs/prism-ui. MFEs consume tokens via CSS variables and components via a published package. The Host sets theme tokens at the root; MFEs inherit automatically.

/* libs/prism-ui/src/styles/tokens.css */
:root {
	--color-primary: #2563eb;
	--color-surface: #f8fafc;
	--color-text: #0f172a;
	--radius-md: 0.5rem;
	--space-md: 1rem;
}

.dark {
	--color-primary: #60a5fa;
	--color-surface: #0b1220;
	--color-text: #e2e8f0;
}

Versioning Shared Components

Version shared UI libraries with semver and publish on a cadence. For breaking changes, announce deprecations early and keep compatibility for at least one minor release.

// v2.3.0: Button adds intent + appearance
type ButtonPropsV2 = {
	intent?: 'primary' | 'secondary' | 'danger';
	appearance?: 'solid' | 'outline';
	variant?: 'solid' | 'outline'; // Deprecated
};

Auth Integration

The Host owns the session and exposes identity state to MFEs via context.

  • Host exchanges credentials for access + refresh tokens
  • Refresh tokens live in httpOnly cookies
  • Access tokens are short-lived (memory or httpOnly cookies in production)
  • MFEs read auth state from context and call shared auth utilities
// libs/identity-utils/src/lib/IdentityContext.tsx
interface IdentityContextType {
	user: User | null;
	isAuthenticated: boolean;
	login: (credentials: Credentials) => Promise<void>;
	logout: () => void;
}

const IdentityContext = createContext<IdentityContextType | undefined>(
	undefined,
);

export const useIdentity = (): IdentityContextType => {
	const context = useContext(IdentityContext);
	if (!context) {
		throw new Error('useIdentity must be used within IdentityProvider');
	}
	return context;
};

Route protection stays in the Host:

const RequireIdentity: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const { isAuthenticated } = useIdentity();
  return isAuthenticated ? <>{children}</> : <Navigate to="/login" />;
};

Team Independence (Build/Test/Deploy)

  • Each MFE builds and deploys as its own artifact
  • The Host deploys independently and reads a versioned MFE manifest at runtime
  • MFEs can ship without Host changes unless contracts change
  • Shared libraries follow a published, versioned release process

Operational Details

  • Shared dependencies: React/React-DOM are singletons with strict versions
  • CSS isolation: CSS Modules + BEM in MFEs, tokens only in libs/prism-ui
  • Integration testing: contract tests + E2E smoke against a staging manifest
  • Deployment model: each MFE ships to its own CDN path; Host updates manifest
  • Security: httpOnly cookies, strict CORS, CSP allowlists for MFE origins

Snippets and Examples

Runtime manifest:

{
	"telemetry": "https://cdn.example.com/mfe/telemetry/1.6.0/remoteEntry.js",
	"orchestration": "https://cdn.example.com/mfe/orchestration/2.2.3/remoteEntry.js",
	"admin": "https://cdn.example.com/mfe/admin/1.3.4/remoteEntry.js",
	"compliance": "https://cdn.example.com/mfe/compliance/0.9.7/remoteEntry.js"
}

Concrete Repo Topology

  • Host application: apps/host
  • Remote applications: apps/telemetry, apps/orchestration, apps/admin, apps/compliance
  • Shared libraries:
    • libs/prism-ui: design system components and tokens
    • libs/identity-utils: auth context and route protection
    • libs/shared-contracts: shared TypeScript types

Build and Dev Workflow

Nx orchestrates builds, tests, and linting across the workspace. Rspack handles bundling and dev servers. Module Federation wiring is handled by Nx plugins.

Dev server ports:

  • Host: 4200
  • Orchestration: 4201
  • Telemetry: 4202
  • Admin: 4203
  • Compliance: 4204

Module Federation config:

  • Host remotes: apps/host/module-federation.config.ts
  • Remote exposes: apps/*/module-federation.config.ts

Rspack config:

  • Host: apps/host/rspack.config.ts
  • Host prod overrides: apps/host/rspack.config.prod.ts
  • Remotes: apps/*/rspack.config.ts

Host Rspack (example)

import { composePlugins, withNx, withReact } from '@nx/rspack';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
import { REACT_VERSION } from '@shared-config/versions';

const config = composePlugins(withNx(), withReact(), (config) => {
	config.plugins.push(
		new ModuleFederationPlugin({
			name: 'host',
			remotes: {
				telemetry:
					'telemetry@https://cdn.example.com/mfe/telemetry/remoteEntry.js',
				orchestration:
					'orchestration@https://cdn.example.com/mfe/orchestration/remoteEntry.js',
				admin: 'admin@https://cdn.example.com/mfe/admin/remoteEntry.js',
				compliance:
					'compliance@https://cdn.example.com/mfe/compliance/remoteEntry.js',
			},
			shared: {
				react: {
					singleton: true,
					requiredVersion: REACT_VERSION,
					strictVersion: true,
				},
				'react-dom': {
					singleton: true,
					requiredVersion: REACT_VERSION,
					strictVersion: true,
				},
			},
		}),
	);
	return config;
});

export default config;

Tools and Frameworks Rationale

Nx: Build and Dev Orchestration

We chose Nx monorepo over polyrepo because there are four teams and four remotes. Polyrepo would add coordination cost without delivering meaningful benefits at this scale.

Rspack: Bundling and Dev Server

Rspack is faster than webpack for our workload and stays mostly compatible with Webpack config patterns. It also works well with Module Federation via @module-federation/enhanced.

Module Federation: Remote Loading

Module Federation is selected over Single-SPA or iframes because:

  • Native runtime sharing
  • Smaller bundles from shared dependencies
  • No framework overhead
  • No special communication layer needed

CI/CD Pipeline (per MFE)

name: Deploy MFE
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Derive affected projects
        run: npx nx affected:apps --base=origin/main~1

      - name: Build affected MFEs
        run: npx nx affected:build --base=origin/main~1

      - name: Run affected tests
        run: npx nx affected:test --base=origin/main~1

      - name: Deploy to CDN
        run: ./scripts/deploy-mfe.sh

      - name: Update manifest
        run: ./scripts/update-manifest.sh

Deployment Model

graph LR
  A[Feature] --> B[Staging]
  B --> C[Production]
  B --> D[Canary]

Summary

This architecture scales when multiple teams need to move quickly without coordinating every release. The key is discipline around shared contracts, clear ownership boundaries, and a predictable deployment process.

← Back to all posts