How to Load an Angular App Without Using SystemJS in Single-spa

When people get started with the whole concept of micro frontends, single-spa is typically the first framework that comes to mind. It is, indeed, a fine name in the market because it allows you to run two or more frameworks or libraries Angular, React, Vue, or vanilla JavaScript in one browser window while also giving them their own DOM. The catch? Most older tutorials show the Angular integration with SystemJS.

But with the current scenario, SystemJS is no more the only way to load apps (and definitely not the best way). With the evolution of Webpack, Module Federation, and ES modules, we can now directly package the Angular apps to integrate them into single-spa without using SystemJS as some sort of a middleman.

Let us now take our sleeves up and actually learn how to do this.

Why Was SystemJS Used in the First Place?

To understand these glitches coming up the pop-ups with errors, really it helps to understand why people use SystemJS so often.

SystemJS was supposed to be a universal loader that retrieved and executed different app bundles on demand.

Single-spa relied on it because micro frontends often had to dynamically load apps built in different frameworks.

Old Angular builds just weren’t compatible with dynamic import, so SystemJS filled the gap.

Fast forward to today: Angular CLI supports Webpack 5 and Module Federation, meaning you don’t need SystemJS anymore to load your micro frontend; hence the error you see is basically because single-spa is expecting SystemJS, and you’re working with the more modern build.

The Error in Action

Any regular developer would run into the following:

single-spa error: Cannot load application ‘angular-app’ without SystemJS

This is such an irritant error because you already have an Angular build, and all you want is to shove it into single-spa. Where you should take heart is in the fact that there are alternatives to this that are fairly easy.

How to Load Angular Apps Without SystemJS

Let us explore all the options available.

Webpack Module Federation

This is by far the cleanest and future proof solution.

With Module Federation, your Angular app can be exposed as a remote module that single-spa can import dynamically.

The steps:

Add @angular-architects/module-federation
plugin to your Angular project.

ng add @angular-architects/module-federation

In your webpack.config.js, configure a remote entry:

const ModuleFederationPlugin = require(“webpack”).container.ModuleFederationPlugin;

module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "angularApp",
filename: "remoteEntry.js",
exposes: {
"./AppModule": "./src/app/app.module.ts",
},
shared: {
"@angular/core": { singleton: true },
"@angular/common": { singleton: true },
"@angular/router": { singleton: true },
},}),],};

Build your Angular project. This produces remoteEntry.js file.

In your single spa root config, load it with a normal import:

registerApplication({
name: "angularApp",
app: () => import("angularApp/AppModule"),
activeWhen: ["/angular"],
});

No SystemJS needed. Instead, you will be using the modern JavaScript import way.

Single-spa-angular Direct

Should you want to go without Module Federation, life will expect you to export lifecycle methods into Angular.

Inside your Angular app main.single-spa.ts:

import { singleSpaAngular } from "single-spa-angular";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from "./app/app.module";
const lifecycles = singleSpaAngular({
bootstrapFunction: () => platformBrowserDynamic().bootstrapModule(AppModule),
template: "",
});
export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;

Then in your root config, simply import this module:

import * as angularApp from “angular-app”;

registerApplication({
name: "angularApp",
app: () => Promise.resolve(angularApp),
activeWhen: ["/angular"],
});

The difference here is you are bundling the Angular app with Webpack (or the default Angular CLI build) and importing it directly. Again, no SystemJS.

Import Maps (Optional Trick)

The other way would be to rely on import maps, which modern browsers now support. Import maps tell the browser, “when you see angular-app, fetch this file.”

Example into your root index.html:

And then in your single-spa config:

registerApplication({
name: "angular-app",
app: () => import("angular-app"),
activeWhen: ["/angular"],
});

This way, SystemJS is avoided, but flexibility is maintained for mapping modules.

Best Practices

  1. Use one approach. Mixing SystemJS, Module Federation, and import maps creates unnecessary confusion.
  2. Angular versions aligned. Ensure that any Angular-based app in your micro frontends setup shares the same versions of Angular core/common.
  3. Use Federation when scalability is expected. Federation pays off when expecting multiple Angular apps or shared libraries.
  4. Test in isolation. Always test your Angular app stand-alone, before wiring it into single-spa; if it doesn’t bootstrap stand-alone, it won’t mount properly in single-spa.

Example Scenario

You have a few apps on your team:

React dashboard

Angular admin panel

Vue chat widget.

Previously, one would probably use SystemJS to load all three. But with a change to Webpack Module Federation:

-Each app is built into its own remote entry.

-The single-spa root config simply imports each of them.

-No SystemJS anymore, less config, and faster runtime.