name: microfrontend-nx description: NX monorepo microfrontend patterns for the Breeze Airways ops-fe workspace. Enforces Module Federation architecture, feature-scoped library structure, shared platform conventions, and routing rules. Use when writing or modifying React/TypeScript code in the operations frontend.
Microfrontend NX Skill — Operations Frontend
Patterns and conventions for the Breeze Airways ops-fe NX monorepo. This workspace implements a Module Federation microfrontend architecture with a single host app (ns2) orchestrating feature domains via React 19, Rspack, and TypeScript.
Architecture Overview
A host application loads feature domains as local routes and remote MFE applications at runtime via Module Federation.
Browser (SPA)
├── ns2 (HOST) :4200 — BrowserRouter
│ ├── /access-mgmt, /disruption, /crew-notifications, ... (local features)
│ └── /flight-story/* → React.lazy(() => import('flight_story/Module'))
│ ↓ Module Federation runtime loading
├── flight_story (REMOTE) :4201 — HashRouter, exposes ./Module
└── turn (STANDALONE) :4200 — No Module Federation
Key rules:
- Host uses
BrowserRouter, remotes useHashRouter(prevents route collisions) - Host owns the URL pathname (
/flight-story), remote owns the hash fragment (/#/details/123) - Shared singletons (React, Apollo, MSAL) resolve from the host — not duplicated
- All apps are client-rendered SPAs (no SSR), all React 19
Directory Structure
ops-fe/
├── apps/
│ ├── ns2/ # HOST application
│ │ ├── module-federation.config.ts
│ │ ├── rspack.config.ts
│ │ ├── rspack.config.prod.ts
│ │ ├── project.json
│ │ └── src/
│ │ ├── bootstrap.tsx # MSAL init + app mount
│ │ ├── main.ts # Entry point (import bootstrap)
│ │ └── app/
│ │ └── app.tsx # Routes + lazy remote import
│ │
│ ├── flight_story/ # REMOTE application
│ │ ├── module-federation.config.ts
│ │ ├── rspack.config.ts
│ │ ├── rspack.config.prod.ts
│ │ ├── project.json
│ │ └── src/
│ │ ├── bootstrap.tsx
│ │ ├── main.ts
│ │ ├── remote-entry.ts # MFE entry (re-exports app)
│ │ └── app/
│ │ └── app.tsx # HashRouter + routes
│ │
│ └── turn/ # STANDALONE application
│
├── libs/
│ ├── shared/ # Platform-level shared code
│ │ ├── shared-ui/ # Reusable UI components
│ │ ├── shared-hooks/ # React hooks
│ │ ├── shared-models/ # Types and interfaces
│ │ ├── shared-services/ # Business logic helpers
│ │ ├── shared-utils/ # Utility functions, clientFactory
│ │ ├── shared-configs/ # MSAL, Sentry, NewRelic, MFE config
│ │ ├── shared-contexts/ # React Contexts
│ │ ├── shared-graphql/ # GraphQL queries/mutations
│ │ └── shared-assets/ # Icons, SVGs (via SVGR)
│ │
│ ├── {feature}/ # Feature-scoped libraries
│ │ ├── {feature}-components/ # UI components for this domain
│ │ └── {feature}-hooks/ # Data hooks for this domain
│ │
│ └── ns2/ # Host app-specific libs
│ ├── ns2-components/
│ └── ns2-hooks/
│
├── environments/
│ ├── environment.ts # Dev/staging config
│ └── environment.cicd.ts # CI/CD config
│
├── nx.json
├── tsconfig.base.json
├── package.json
└── tailwind.config.js
Feature Domains
18 feature domains, each with its own libs/{feature}/ directory:
| Domain | Library Path |
|---|---|
| access-management | libs/access-management/ |
| bag-bungler | libs/bag-bungler/ |
| bag-tag-scanner | libs/bag-tag-scanner/ |
| crew-notifications | libs/crew-notifications/ |
| disruption-log | libs/disruption-log/ |
| employee-travel | libs/employee-travel/ |
| far-monitor | libs/far-monitor/ |
| flight-line | libs/flight-line/ |
| flight-story | libs/flight-story/ |
| fuel-slip | libs/fuel-slip/ |
| guest-notifications | libs/guest-notifications/ |
| load-sheet | libs/load-sheet/ |
| notifications | libs/notifications/ |
| ns2 | libs/ns2/ |
| ssim | libs/ssim/ |
| ssr-portal | libs/ssr-portal/ |
| turn | libs/turn/ |
Core Patterns
1. TypeScript Path Aliases
All imports use @ops-fe/ scoped aliases defined in tsconfig.base.json:
// Shared platform libraries
import { SomeComponent } from '@ops-fe/shared-ui';
import { useAuth } from '@ops-fe/shared-hooks';
import { FlightModel } from '@ops-fe/shared-models';
import { clientFactory } from '@ops-fe/shared-utils';
import { msalConfig } from '@ops-fe/shared-configs';
import { AppContext } from '@ops-fe/shared-contexts';
import { GET_FLIGHTS } from '@ops-fe/shared-graphql';
// Feature-scoped libraries
import { FlightCard } from '@ops-fe/{feature}-components';
import { useFlightData } from '@ops-fe/{feature}-hooks';
// Assets (wildcard path)
import SomeIcon from '@ops-fe/shared-assets/icons/SomeIcon.svg';
Rules:
- Never use relative paths across library boundaries — always use
@ops-fe/aliases - Each library exports through a single
src/index.tsbarrel file - Feature libs only import from
shared/libs, never from other feature libs
2. Module Federation Configuration
Host (apps/ns2/module-federation.config.ts):
import { ModuleFederationConfig } from '@nx/module-federation';
import { MFE_PACKAGE_CONFIG } from '@ops-fe/shared-configs';
const config: ModuleFederationConfig = {
name: 'ns2',
remotes: ['flight_story'],
shared: (name, config) => {
if (MFE_PACKAGE_CONFIG.includes(name)) return false;
return config;
},
};
export default config;
Remote (apps/flight_story/module-federation.config.ts):
const config: ModuleFederationConfig = {
name: 'flight_story',
exposes: {
'./Module': './src/remote-entry.ts',
},
shared: (name, config) => {
if (MFE_PACKAGE_CONFIG.includes(name)) return false;
return config;
},
};
export default config;
Singleton exclusions (libs/shared/shared-configs/src/lib/mfe-package-config.ts):
export const MFE_PACKAGE_CONFIG: string[] = [
'@apollo/client/link/retry',
'@apollo/client/link/context',
'@radix-ui/react-slot',
'react-phone-number-input/input',
'react-phone-number-input/locale/en',
];
These packages are excluded from Module Federation's singleton sharing to avoid version conflicts.
3. Application Bootstrap Pattern
Every app follows: main.ts → async bootstrap.tsx → <App />
// main.ts — entry point (async boundary for Module Federation)
import('./bootstrap');
// bootstrap.tsx — initialize auth, mount app
import { PublicClientApplication } from '@azure/msal-browser';
import { MsalProvider } from '@azure/msal-react';
const msalInstance = new PublicClientApplication(msalConfig);
// ... MSAL init
root.render(
<MsalProvider instance={msalInstance}>
<App />
</MsalProvider>
);
The async boundary in main.ts is required — it allows Module Federation to negotiate shared dependencies before app code executes.
4. Remote Loading Pattern
Remotes are loaded in the host via React.lazy + Suspense:
// apps/ns2/src/app/app.tsx
const FlightStory = React.lazy(() => import('flight_story/Module'));
// In routes:
<React.Suspense fallback={<LoadingSpinner />}>
<FlightStory />
</React.Suspense>
Always wrap remote components with an error boundary to prevent host crash on remote load failure.
5. Router Isolation
HOST (ns2) — BrowserRouter REMOTE (flight_story) — HashRouter
/flight-story/* ────────────▶ /#/
/access-management/* /#/:id
/disruption-log/* /#/simulator/flights
/crew-notifications/* /#/simulator/load-sheet
...
Rules:
- Host always uses
BrowserRouter - Remote MFEs always use
HashRouter - Never mix router types within a single app
6. Feature-Scoped Library Pattern
Each feature domain isolates its code in libs/{feature}/:
libs/disruption-log/
├── disruption-log-components/
│ └── src/
│ ├── index.ts # Barrel export
│ └── lib/
│ ├── DisruptionForm.tsx
│ └── DisruptionTable.tsx
└── disruption-log-hooks/
└── src/
├── index.ts # Barrel export
└── lib/
├── useDisruptionLog.ts
└── useDisruptionFilters.ts
Rules:
-componentslibs contain React UI components-hookslibs contain data-fetching hooks (Apollo queries, state logic)- Feature libs may import from
shared/but never from other feature libs - Cross-feature logic belongs in a
shared/library
Shared Dependency Resolution
These packages are shared as singletons across host and remotes via Module Federation:
| Package | Role |
|---|---|
react / react-dom |
UI framework (single instance required) |
react-router |
Routing |
@apollo/client |
GraphQL client |
@azure/msal-react |
Authentication |
The MFE_PACKAGE_CONFIG list excludes specific sub-packages from sharing (loaded independently per MFE).
Authentication Flow
bootstrap.tsxinitializes MSALPublicClientApplication- Azure AD login returns JWT with
nameandemployee_number - Token feeds into
MsalProvider(auth state), Apollo (Bearer header), and GrowthBook (user attributes) - Host and remotes share the MSAL instance through Module Federation singletons
- Unauthenticated users see
UnauthenticatedTemplate(login page)
Rspack Configuration
Each app has two Rspack configs:
Development (rspack.config.ts):
const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin');
const { NxModuleFederationPlugin } = require('@nx/module-federation/rspack');
const { NxModuleFederationDevServerPlugin } = require('@nx/module-federation/rspack');
module.exports = {
output: { uniqueName: 'ns2', publicPath: 'auto' },
devServer: { port: 4200, historyApiFallback: true },
plugins: [
new NxAppRspackPlugin({ /* assets, styles, scripts */ }),
new NxReactRspackPlugin({ svgr: true }),
new NxModuleFederationPlugin({ config: mfConfig }),
new NxModuleFederationDevServerPlugin({ config: mfConfig }),
],
};
Production (rspack.config.prod.ts) extends dev config with xxhash64 hashing.
Rules:
- Always use Rspack (not Webpack) —
@nx/rspackplugins - SVGR is enabled (
svgr: true) for importing SVGs as React components publicPath: 'auto'is required for Module FederationhistoryApiFallback: trueis required for SPA routing
Environment Configuration
// environments/environment.ts
export const environment = {
production: false,
azureAd: { clientId: '...', apiScope: '...' },
urls: { ns2: '...', disruptionLog: '...', operationsBackend: '...' },
growthBook: { clientKey: '...' },
firebase: { projectId: '...', vapidKey: '...' },
scandit: { licenseKey: '...' },
newRelic: { licenseKey: '...', accountId: '...' },
};
Rules:
- No secrets in client bundles
- Use
environment.tsfor dev/staging,environment.cicd.tsfor CI/CD - All API URLs come from environment config, never hardcoded
NX Commands Reference
# Serve host with all remotes
npx nx serve ns2
# Serve individual remote
npx nx serve flight_story
# Serve host with specific remote in dev mode
NX_MF_DEV_REMOTES=flight_story npx nx serve ns2
# Build for production
npx nx run-many -t build --configuration=cicd --verbose
# Run all tests
npx nx run-many -t test
# Run tests for a specific library
npx nx test shared-ui
# Run affected tests only
npx nx affected -t test
# Generate a new library
npx nx g @nx/react:lib my-feature-components --directory=libs/my-feature
# Generate a new remote MFE app
npx nx g @nx/react:remote my-remote --host=ns2
# Visualize dependency graph
npx nx graph
Testing
- Unit Tests: Jest +
@testing-library/react. Each library has its own test config via NX project inference. - Component Dev: Storybook (uses Vite builder, separate from Rspack).
- Linting: ESLint (flat config) + Prettier.
- CI:
npx nx run-many -t testruns all affected tests. NX caching skips unchanged projects.
Observability
| Tool | Purpose |
|---|---|
Sentry (@sentry/react) |
Error tracking and source maps |
New Relic (@newrelic/browser-agent) |
Performance monitoring and RUM |
| Firebase | Push notification delivery tracking |
Error Handling
| Scenario | Recovery |
|---|---|
| Remote fails to load | Suspense fallback UI; user can retry navigation |
| Auth token expired | MSAL auto-refreshes; redirect to login on failure |
| GraphQL error | Apollo error policies per query; toast notifications |
| Feature flag unavailable | GrowthBook defaults to false (feature hidden) |
Technology Stack
| Category | Technology |
|---|---|
| Framework | React 19 |
| Language | TypeScript |
| Bundler | Rspack (via @nx/rspack) |
| MFE Runtime | Module Federation (Enhanced) |
| Monorepo | NX |
| Routing | React Router 7 |
| GraphQL | Apollo Client |
| Auth | Azure MSAL |
| Feature Flags | GrowthBook |
| CSS | Tailwind CSS |
| UI Components | MUI |
| Testing | Jest + Testing Library |
| Component Dev | Storybook |
| CI/CD | GitLab CI |
Adding a New Feature Checklist
When adding a new feature domain:
- Generate libraries —
npx nx g @nx/react:lib {feature}-components --directory=libs/{feature}and{feature}-hooks - Add path aliases — Update
tsconfig.base.jsonwith@ops-fe/{feature}-componentsand@ops-fe/{feature}-hooks - Create components — Add UI components in
{feature}-components/src/lib/ - Create hooks — Add data-fetching hooks in
{feature}-hooks/src/lib/ - Export via barrel — Update each library's
src/index.ts - Add route — Add route in
apps/ns2/src/app/app.tsx(or remote app if applicable) - Add shared types — If types are cross-feature, add to
shared-models - Add GraphQL operations — If new queries/mutations, add to
shared-graphqlor feature hooks - Test — Unit tests per library,
npx nx test {feature}-components
Promoting a Feature to Remote MFE
When a feature domain needs independent deployment:
- Generate remote app —
npx nx g @nx/react:remote {feature} --host=ns2 - Add Module Federation config — Create
module-federation.config.tswithexposes: { './Module': './src/remote-entry.ts' } - Use HashRouter — Remote app must use
HashRouter, notBrowserRouter - Create remote-entry.ts — Re-export the app component
- Update host config — Add remote name to
remotesarray in host'smodule-federation.config.ts - Add lazy import in host —
React.lazy(() => import('{feature}/Module'))wrapped inSuspense+ error boundary - Add type declaration — Add
"{feature}/Module"path alias intsconfig.base.json