Skip to content
AI Monorepos Free online conference · June 23 Join us!

Module Federation Consumer and Provider (v23+)

Nx v23 introduces a new generator surface in @nx/react (@nx/react:consumer and @nx/react:provider) that replaces the v22 host and remote generators. Shared logic (federation-name validation, port defaults, version pins) lives in @nx/module-federation so framework-specific generators can build on it. The new model has two roles:

  • Provider - an app that exposes a federated component.
  • Consumer - an app that loads federated components at runtime via a hardcoded PROVIDERS list in src/mf.ts.

Both generators are React-only. The bundler is chosen at generation time and cannot be changed later (the bundler config is too different to switch in place).

The generator emits a plain bundler config (no Nx wrapper). Pick one at generation time:

BundlerPick it when
vite (default)Fastest dev iteration; smallest config surface; widest ecosystem.
rsbuildYou want webpack-class production builds with a small config surface.
rspackYou want full webpack-API compatibility and explicit control of the build.
Terminal window
nx g @nx/react:provider apps/my-provider --bundler=vite

Generated tree:

apps/my-provider/
├── package.json # name, scripts, MF + framework deps
├── vite.config.ts # (or rsbuild/rspack equivalent) with federation plugin
├── index.html
├── src/
│ ├── index.ts # `import('./bootstrap')` indirection (required)
│ ├── bootstrap.tsx # createRoot + render
│ └── App.tsx # the federated component, default export
└── tsconfig.json

The federation plugin exposes ./App (or --exposeName=<your-name>). The expose name stays as you type it for the public MF key (consumers reference <provider>/<exposeName>, so cart-widget is fine); the generated React component and its filename are normalized to PascalCase (CartWidget) so the emitted TypeScript is valid. Consumers register the provider by its remoteEntry.js URL. The provider runs standalone on the configured port (default 5101 for Vite, 3101 for Rsbuild, 8101 for Rspack).

Terminal window
nx g @nx/react:consumer apps/my-consumer --bundler=vite --providerNames=my-provider

Generated tree:

apps/my-consumer/
├── package.json
├── vite.config.ts # NO build-time `remotes:` block
├── index.html
├── src/
│ ├── index.ts # `import('./bootstrap')`
│ ├── bootstrap.tsx # createRoot + render <App />
│ ├── App.tsx # imports + renders one lazy remote per PROVIDERS entry
│ └── mf.ts # hardcoded PROVIDERS list + registerRemotes + lazyProvider helper
└── tsconfig.json

When --providerNames=p1,p2,p3 is passed, the consumer generator also scaffolds a sibling @nx/react:provider app per entry (at apps/p1, apps/p2, apps/p3) and wires their remoteEntry.js URLs into the consumer's PROVIDERS list. Each provider's serve target depends on the consumer's serve, so nx serve p1 brings the consumer up alongside the provider.

Omit the flag and the consumer ships a placeholder my-provider entry in PROVIDERS (no actual provider project is generated).

The consumer's src/mf.ts holds the remote list inline:

// `name` is the provider's federation container name (derived from its
// project name, so it can differ from `alias` - e.g. `myCart` -> `my_cart`).
// `alias` is the key you loadRemote() with. `entry` is the remoteEntry.js URL.
const PROVIDERS: Array<{ alias: string; name: string; entry: string }> = [
{
alias: 'my-provider',
name: 'my_provider',
entry: 'http://localhost:5101/remoteEntry.js',
},
];
registerRemotes(
// For vite providers only - vite emits ESM remoteEntry.js. The generator
// omits `type` for rspack/rsbuild (UMD) so the runtime auto-detects.
PROVIDERS.map((remote) => ({ ...remote, type: 'module' }))
);
export function lazyProvider(alias, exposeName) {
/* lazy + loadRemote */
}

Edit PROVIDERS to point at different providers. The entry is each provider's remoteEntry.js URL - the entry every bundler emits at dev + build time. (The richer mf-manifest.json works for production builds and for rspack/rsbuild dev, but @module-federation/vite only emits it at build time, so the generator uses remoteEntry.js for a consistent dev experience.)

The generated App.tsx renders each provider inside a ProviderBoundary (an inline class that combines Suspense with an error boundary, so one unreachable provider can't unmount the whole tree):

import { lazyProvider } from './mf';
// ...ProviderBoundary class omitted...
const ProviderMyProvider = lazyProvider('my-provider', 'App');
export function App() {
return (
<main>
<h1>my-consumer</h1>
<ProviderBoundary name="my-provider">
<ProviderMyProvider />
</ProviderBoundary>
</main>
);
}

Wrap each <ProviderBoundary> in your router of choice (TanStack Router, React Router, etc.) if you need routing.

The following surfaces are deprecated in v23 and will be removed in v24:

  • @nx/react:host, @nx/react:remote, @nx/react:federate-module
  • @nx/angular:host, @nx/angular:remote, @nx/angular:setup-mf, @nx/angular:federate-module
  • @nx/react:module-federation-dev-server, @nx/react:module-federation-ssr-dev-server, @nx/react:module-federation-static-server
  • @nx/angular:module-federation-dev-server, @nx/angular:module-federation-dev-ssr
  • @nx/rspack:module-federation-dev-server, @nx/rspack:module-federation-ssr-dev-server, @nx/rspack:module-federation-static-server

The most user-visible change: nx serve <host> no longer auto-builds and serves all remotes. With dynamic federation, the relationship is inverted - serve a provider and Nx brings its consumer along (provider.serve.dependsOn = ['<consumer>:serve']). Missing providers don't crash the consumer; they reject at loadRemote time and the generated ProviderBoundary renders an inline fallback.

Angular Module Federation in Nx is no longer supported. Use @angular-architects/native-federation for the supported Angular path going forward.

There is no automated codemod. Existing setups vary widely (custom executors, host orchestration, SSR variants) and a generator would not land cleanly. Migrate manually using the steps below, or paste the AI prompt at the end of this section into Cursor / Claude Code / Copilot to perform the rewrite on your own codebase.

  1. Generate a fresh consumer for each existing host and a provider for each existing remote, using the same bundler you were on (or upgrade to Vite). Use a temporary directory so nothing overwrites the originals.
  2. Delete module-federation.config.ts from each app.
  3. Replace the bundler config in each app with the generated one. Port any custom withModuleFederation wrapping to inline plugin options.
  4. Convert each consumer's static remotes: list into entries in src/mf.ts's PROVIDERS constant (URLs point at each provider's remoteEntry.js).
  5. Rewrite import('remote-name/Module') calls to lazyProvider('remote-name', 'Module') from src/mf.ts.
  6. Drop the customWebpackConfig block from each project.json. Replace the serve target with one that runs vite / rsbuild / rspack directly via nx:run-commands.
  7. On each provider's serve target, add dependsOn: ['<consumer>:serve'] if you want nx serve <provider> to spin up the consumer alongside.

Paste this into your AI assistant of choice, adjusted with your project paths:

You are migrating an Nx workspace from the deprecated `@nx/react:host` / `@nx/react:remote` generators to the new
`@nx/react:consumer` / `@nx/react:provider` generators (Nx v23+).
For each `host` app in `apps/`:
1. Delete `module-federation.config.ts`.
2. Replace the bundler config with a plain `vite.config.ts` / `rsbuild.config.ts` / `rspack.config.ts` that uses
`@module-federation/vite` (vite), `@module-federation/rsbuild-plugin` (rsbuild), or
`@module-federation/enhanced/rspack` (rspack) directly. No `withModuleFederation` wrappers.
3. Create `src/mf.ts` with a `PROVIDERS` constant - an array of `{ alias, name, entry }` where `name` is the
provider's federation container name (derived from its project name, so it can differ from `alias`, e.g.
`myCart` -> `my_cart`) and `entry` is its `remoteEntry.js` URL. At module init, call `registerRemotes` from
`@module-federation/runtime` with one entry per provider (set `type: 'module'` for vite providers; omit it for
rspack/rsbuild). Export `lazyProvider(alias, exposeName)` that returns `React.lazy(() => loadRemote(...))`.
4. Replace all `import('remote-name/Module')` calls with `lazyProvider('remote-name', 'Module')`.
5. Drop the `customWebpackConfig` block from `project.json`. Replace `serve` and `build` targets with `nx:run-commands`
invocations of the bundler directly.
For each `remote` app, do the equivalent provider conversion: drop the federation config file, write a plain bundler
config with the federation plugin exposing the same modules, and emit a standalone `index.html` + `src/index.ts` ->
`src/bootstrap.tsx` indirection. Add `serve.dependsOn: ['<host>:serve']` to the provider's project.json if you want
`nx serve <provider>` to also start the consumer.
Do not orchestrate remotes from the host. Each app is served independently; missing remotes render via the consumer's
Suspense + ErrorBoundary.

SSR is not first-classed in the new generators. The deprecated module-federation-ssr-dev-server executors are gone. If you need SSR with federation, see the upstream Module Federation SSR guide and wire it yourself on top of the generated consumer skeleton.

A worked reference workspace covering all three bundlers, dynamic federation, and Angular Native Federation lives at the mf-examples repository (see apps/nx-react-vite/ for the canonical Nx-wired setup).