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.
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 tokenslibs/identity-utils: auth context and route protectionlibs/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.