Since version 20, Nx ships executors that wrap rspack and rsbuild. The latter layer sits on top of rspack and gives you a fast, pre-configured setup for common build scenarios. Because of Nx's support here, wiring Module Federation onto rspack is straightforward. Even though there is still no official rspack-powered executor for Angular projects in Nx, Colum Ferry from the Nx team has just published an early build of the open-source @ng-rsbuild/plugin-nx package. It plugs the gap by relying on rsbuild, and it already comes with SSR support. Since it's a community project, @ng-rsbuild/plugin-nx hasn't been adopted into the official Nx feature set. That said, having someone with Colum's background behind it immediately lends credibility. Below, I'll demonstrate how to combine this package with Nx, Angular, and Module Federation to assemble a Micro Frontend architecture. For that, I'm going to use the same demo setup I usually reach for: Demo Application 📂 Source Code

Disclaimer: @ng-rsbuild/plugin-nx is currently in Alpha. So things might change and be improved. I will keep you posted here.

Preparing an Nx Workspace

To kick things off, install the Angular-focused rsbuild plugin into a fresh Nx 19 workspace, then use it to generate a `shell` and a Micro Frontend called `mfe1`:
npx nx add @ng-rsbuild/plugin-nx

npx nx g @ng-rsbuild/plugin-nx:application shell
npx nx g @ng-rsbuild/plugin-nx:application mfe1
Because we want to use dynamic Federation, we also have to pull in the @module-federation/enhanced package, which hosts the Module Federation runtime:
npm i @module-federation/enhanced

Building the Micro Frontend

The Micro Frontend's `AppComponent` is kept deliberately minimal:
@Component({
  imports: [],
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})
export class AppComponent {
  title = 'myapp';

  search(): void {
    alert('Not implemented!');
  }
}

// Add default export:
export default AppComponent;
Treat this simple component as a stand-in for a larger Micro Frontend that might orchestrate multiple use cases through routing. Also note that the example relies on a default export for the AppComponent. That little choice makes lazy-loading it from the shell a bit less verbose. For completeness, here's the component's template:
<div id="container">
    <h1>Select a Flight</h1>
    <div>
        <input type="text" placeholder="From">
    </div>
    <div>
        <input type="text" placeholder="To">
    </div>
    <div>
        <button (click)="search()">Search</button>
    </div>
</div>

Switching to Async Bootstrapping

Our Module Federation setup requires asynchronous bootstrapping. To achieve that, copy the contents of mfe1/src/main.ts into a fresh file called mfe1/src/bootstrap.ts, then just import that file from the original one:
// mfe1/src/main.ts
import('./bootstrap');
The key detail here is to use a dynamic — and therefore asynchronous — import. That gives Module Federation the chance to initialize and to handle loading of shared packages across the rest of the application.

More on this: Angular Architecture Workshop (online, interactive, advanced)

Become an expert for enterprise-scale and maintainable Angular applications with our Angular Architecture workshop!Nx and Angular with Rspack and Module Federation — figure 2

English Version | German Version

Adjusting the Micro Frontend Configuration

To set up Module Federation, we now need to edit the rsbuild configuration file that @ng-rsbuild/plugin-nx created in mfe1/rsbuild.config.ts:
import { createConfig } from '@ng-rsbuild/plugin-angular';

// eslint-disable-next-line @nx/enforce-module-boundaries
import { shareAll } from '../mf.tools';

export default createConfig({
  browser: './src/main.ts',
}, {
  server: {
    port: 4201
  },
  tools: {
    rspack: {
      output: {
        uniqueName: 'mfe1',
        publicPath: 'auto',
      },
    },
  },  
  moduleFederation: {
    options: {
      name: 'mfe1',
      filename: 'remoteEntry.js',
      exposes: {
        './Component': './src/app/app.component.ts'
      },
      shared: {
        ...shareAll({
          singleton: true,
          strictVersion: true,
        })
      }
    }
  }
});
Aside from moving the port to 4201, we need to define a publicPath. This URL points to where the Micro Frontend's bundles are deployed. The special value auto tells the loader to infer that URL at runtime. Should you want to control the logic that does this inference, you can bind the getPublicPath property to custom JavaScript executed at runtime. The rest of the configuration is standard Module Federation territory:
  • The Micro Frontend is registered under the unique name mfe1.
  • Module Federation is instructed to place the build-time metadata into remoteEntry.js.
  • The AppComponent is exposed via the name ./Component, allowing the shell to load it at runtime.
  • All dependencies are shared with the shell and other Micro Frontends at runtime.
The shareAll helper keeps the boilerplate down. Without it, we'd have to list every package we want to share:
shared: {
  '@angular/animations': { singleton: true, strictVersion: true },
  '@angular/common': { singleton: true, strictVersion: true },
  '@angular/compiler': { singleton: true, strictVersion: true },
  '@angular/core': { singleton: true, strictVersion: true },
  '@angular/forms': { singleton: true, strictVersion: true },
  '@angular/platform-browser': { singleton: true, strictVersion: true },
  '@angular/platform-browser-dynamic': { singleton: true, strictVersion: true },
  '@angular/router': { singleton: true, strictVersion: true },
  '@module-federation/enhanced': { singleton: true, strictVersion: true },
  'rxjs': { singleton: true, strictVersion: true }
}
Setting both singleton and strictVersion to true guarantees that only one copy of each dependency can be loaded at runtime. If multiple non-compatible versions of the same library end up in the graph, the app throws an Exception during startup. When strictVersion is false — or absent — that same situation just surfaces as a warning in the browser console. Manually maintaining that complete listing is error-prone, which is exactly what the shareAll utility solves: it generates one entry for each dependency listed in the project's package.json. This lightweight implementation ignores secondary entry points like @angular/common/http, though — those still need to be appended manually. My hunch is that @ng-rsbuild/plugin-nx or Nx itself will eventually provide such logic that also automates handling secondary entry points out of the box as well as further API sugar for simplifying the configuration.

A Note on Pinning Angular Package Versions

The shareAll helper picks up version numbers from your package.json. For any Angular-related package, it's wise to pin them by stripping off prefixes like ^ or ~. Here's why: a compiled Angular app expects to run against the exact same Angular version it was compiled with, because the emitted code touches Angular's private internals. Those private APIs aren't bound by semver guarantees. The encouraging part is that there are hints this constraint may loosen in the future.

Preparing the Shell

The shell's rsbuild configuration, located in shell/rsbuild.config.ts, closely mirrors the Micro Frontend's setup:
import { createConfig } from '@ng-rsbuild/plugin-angular';

// eslint-disable-next-line @nx/enforce-module-boundaries
import { shareAll } from '../mf.tools';

export default createConfig({
  browser: './src/main.ts',
}, {
  moduleFederation: {
    options: {
      name: 'shell',
      shared: {
        ...shareAll({
          singleton: true,
          strictVersion: true,
        })
      }
    }
  }
});
Since the shell doesn't expose any modules, there's no exposes section. The shell also needs the same asynchronous bootstrapping treatment covered earlier. Its main.ts leans on the Module Federation runtime to get Federation going, essentially punching in the unique names of the Micro Frontends and where their remote entries live:
import { init } from '@module-federation/enhanced/runtime';

init({
  name: 'shell',
  remotes: [
    {
      name: "mfe1",
      entry: "http://localhost:4201/remoteEntry.js",
    }
  ],
});

import('./bootstrap');
Module Federation 2 swaps JavaScript-based remote entries for a JSON manifest. I'll dig deeper into that in a follow-up piece. Once Federation is up and running, the app hands over control to bootstrap.ts, which holds the bootstrapping logic that normally lives in main.ts.

Bringing in the Micro Frontend

To mount the Micro Frontend, the demo relies on a lazy route declared in shell/src/app/app.routes.ts:
import { Route } from '@angular/router';
import { HomeComponent } from './home.component';
import { loadRemote } from '@module-federation/enhanced/runtime';
import { Type } from '@angular/core';

export const appRoutes: Route[] = [
    {
        path: '',
        pathMatch: 'full',
        component: HomeComponent
    },
    {
        path: 'mfe1',
        loadComponent: () => loadRemote('mfe1/Component') as Promise<Type<unknown>>
    }
];
The loadRemote function from the Federation runtime fetches the AppComponent from the Micro Frontend. Because that component has a default export, we don't have to name it explicitly. The string mfe1/Component references the module that mfe1 exposes as ./Component. The sample omits a fallback when lazy-loading mfe1 fails. In a real-world app, you'd want a catch handler, plus logic for handling the null result that signals loadRemote didn't succeed. To see the Micro Frontend in action, we still need a routerLink and a router-outlet in app.component.ts:
<ul>
    <li><a routerLink="/">Home</a></li>
    <li><a routerLink="/mfe1">Flights</a></li>
</ul>

<router-outlet></router-outlet>

Testing the Setup

To launch the Micro Frontend solution, fire up both applications in separate terminals:
nx dev mfe1 -o
nx dev shell -o

Extras: Sharing Libraries and Data

🔀 Branch: shared-lib Up to this point, the sharing focused on third-party npm packages. Naturally, our own npm packages can be shared just the same. In fact, such shared packages are a convenient channel for communication between Micro Frontends. One simple technique: drop an Angular service into the shared package, write data to it from one Micro Frontend, and read that data from others. Since services are Singletons, that works. To push updates, pair the service with Observables, Subjects, or Signals, and Micro Frontends can get notified instantly. If publishing npm packages isn't desirable, a monorepo-internal library works just as well. For example, we could add an auth library that tracks the current user name:
nx g lib auth
A small service that stores the user name could look like this:
import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AuthService {
  userName = signal('');
}
We also need to make sure this service is re-exported through the library's public API:
export * from './lib/auth/auth.component';

// Add this export:
export * from './lib/auth/auth.service';
Next, mark the auth library as shared. That translates into adding the following entry to the shared section of both rsbuild configurations:
[...]
shared: {
  ...shareAll({
    singleton: true,
    strictVersion: true,
  }),
  '@rspack-demo/auth': {
    singleton: true,
    strictVersion: true,
    version: '0.0.0',
    requiredVersion: '0.0.0',
    import: '../auth/src/index.ts',
  },
}
[...]
Since there's no package.json to clue us in on the library's entry point, we have to spell it out with the import property. We also have to declare the current version as well as the accepted version range via requiredVersion. In both spots, the example uses 0.0.0, assuming that a library kept inside a monorepo doesn't carry a version. For more elaborate cases, you might want to assign version numbers, so that, for example, only the highest version across deployed Micro Frontends is shared at runtime. 🔀 Branch: shared-lib-helper This kind of configuration can also be generated programmatically with the shareAll helper. The shared-lib-helper branch carries a version of shareAll that goes beyond package.json dependencies. It also picks up every path mapping in tsconfig.base.json that points to a repo-internal library:
[...]
shared: {
  // 
  // now, shareAll takes care of both,
  // packages and repo-internal libs
  //
  ...shareAll({
    singleton: true,
    strictVersion: true,
  }),
},
[...]

Observations and Future Directions

Thanks to the community-maintained @ng-rsbuild/plugin-nx package, which is released by Nx team member Ferry Colum, developers can now leverage rsbuild for Angular projects within their Nx workspaces. This integration yields faster build times, as rsbuild and its underlying rspack engine—like other modern build tools—are compiled to native machine code and engineered with parallelism as a core principle.

Whether this compellingly argues for a shift away from the Angular CLI's esbuild-based ApplicationBuilder remains to be seen. That builder currently serves as the de facto standard in the Angular ecosystem, offering comparable performance traits (native compilation and parallel execution) while powering core capabilities like SSR, Hybrid Rendering, and Incremental Hydration.

Even putting raw performance aside, however, this package holds significant value. It injects healthy competition into the tooling landscape, which can spur innovation on both sides. Moreover, it establishes a robust foundation for adopting the newest iterations of Module Federation, bringing its innovations to the Angular realm in a performant manner. That said, while both approaches carry their own trade-offs, the Native Federation strategy also shows considerable promise.

Another notable benefit of the rsbuild integration is its design goal of compatibility with the majority of webpack plugins, which considerably eases the migration path for existing setups toward a swifter, more modern build pipeline. Furthermore, rspack enjoys backing from Bytedance, the organization behind TikTok and other products, and already boasts a thriving ecosystem that includes tools like the rspress static site generator and the rsdoctor build analyzer.

The prospect of standardizing on a single build tool across multiple frameworks within an Nx workspace is quite appealing. Providing users with flexibility in selecting their preferred tools represents a core benefit of Nx. As Colum noted, this package also carries potential to nurture a more robust plugin community, making it easier for developers to tap into the wider array of rsbuild plugins.

To conclude, a massive thank-you is owed to Colum Ferry. The substantial time and effort he poured into this integration ultimately bestows all the aforementioned advantages upon the entire community.