DEV Community

Cover image for Micro Frontends in Angular: A Simple Setup You Can Actually Follow
SAVITA WADJE
SAVITA WADJE

Posted on

Micro Frontends in Angular: A Simple Setup You Can Actually Follow

Micro frontends sound complicated the first time you read about them, separate teams, separate deployments, separate frameworks even. But the core idea is simpler than it sounds, and getting a basic setup running in Angular does not take as much effort as most articles make it seem.

What a micro frontend actually is

Instead of building one large Angular app that owns every feature, you split the app into smaller, independently built and deployed pieces. Each piece can be developed, tested, and released by a different team, on its own timeline, without waiting on everyone else.

A shell app (sometimes called the host) loads these smaller apps (called remotes) at runtime and stitches them together into one experience for the user.

Why bother with this

For a small app, honestly, you probably should not. Micro frontends add real complexity, and for a single team working on one codebase, a normal monolith Angular app is usually the better call.

Where it starts to make sense:

Multiple teams working on the same product, each owning a different section
Different parts of the app needing to release on different schedules
Large legacy apps where a full rewrite is not realistic, but individual sections can be modernized one at a time

The tool that makes this practical: Module Federation

Module Federation, originally a Webpack feature, lets one application load code from another application at runtime, without bundling it together at build time. Angular supports this through the @angular-architects/module-federation package, which handles most of the configuration for you.

Setting up a basic example

Let's build the simplest possible version: a shell app that loads one remote app containing a single feature.

Step 1: Create both apps

ng new shell-app --routing --style=css
ng new remote-app --routing --style=css
Enter fullscreen mode Exit fullscreen mode

Step 2: Add Module Federation to both

cd shell-app
ng add @angular-architects/module-federation --project shell-app --port 4200 --type host

cd ../remote-app
ng add @angular-architects/module-federation --project remote-app --port 4201 --type remote
Enter fullscreen mode Exit fullscreen mode

This generates a webpack.config.js in each project and wires up the basic federation setup automatically.

Step 3: Configure the remote app

In remote-app/webpack.config.js, expose the module you want the shell to be able to load:

module.exports = withModuleFederationPlugin({
  name: "remoteApp",
  exposes: {
    "./Module": "./src/app/remote/remote.module.ts",
  },
  shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: "auto" }),
  },
});
Enter fullscreen mode Exit fullscreen mode

Create a simple feature module in the remote app to actually expose:

// remote/remote.module.ts
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule } from "@angular/router";
import { RemoteComponent } from "./remote.component";

@NgModule({
  declarations: [RemoteComponent],
  imports: [
    CommonModule,
    RouterModule.forChild([{ path: "", component: RemoteComponent }]),
  ],
})
export class RemoteModule {}
Enter fullscreen mode Exit fullscreen mode
// remote/remote.component.ts
import { Component } from "@angular/core";

@Component({
  selector: "app-remote",
  template: `<h2>Hello from the Remote App</h2>`,
})
export class RemoteComponent {}
Enter fullscreen mode Exit fullscreen mode

Step 4: Configure the shell app

In shell-app/webpack.config.js, tell the shell where to find the remote:

module.exports = withModuleFederationPlugin({
  remotes: {
    remoteApp: "remoteApp@http://localhost:4201/remoteEntry.js",
  },
  shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: "auto" }),
  },
});
Enter fullscreen mode Exit fullscreen mode

Then add a lazy-loaded route in the shell that points to the remote module:

// shell-app routing
const routes: Routes = [
  {
    path: "remote-feature",
    loadChildren: () =>
      loadRemoteModule({
        remoteEntry: "http://localhost:4201/remoteEntry.js",
        remoteName: "remoteApp",
        exposedModule: "./Module",
      }).then((m) => m.RemoteModule),
  },
];
Enter fullscreen mode Exit fullscreen mode

Step 5: Run both apps

# terminal 1
cd remote-app && ng serve

# terminal 2
cd shell-app && ng serve
Enter fullscreen mode Exit fullscreen mode

Visit the shell app and navigate to /remote-feature. Angular loads the remote app's code at runtime and renders it right inside the shell, even though it is a completely separate build.

The part that actually trips people up

Shared dependencies. Both apps need to agree on shared library versions, especially Angular itself. If the shell and remote load different versions of Angular, you end up with duplicated frameworks running side by side, which is slow and buggy. The sharedconfig with singleton: true handles this, but mismatched versions between the two apps are the most common source of runtime errors when people first try this.

Routing across apps. The shell owns the top level routes. Remote apps expose modules, they do not control the browser URL directly. Keeping this boundary clear early on avoids a lot of confusion later.

Independent deployment. In a real setup, the remote app gets built and deployed separately, and its remoteEntry.js URL points to wherever it actually lives in production, not localhost. That URL becomes the contract between the shell and the remote.

When this is worth the complexity

If you are working solo or on a small team, stick with a normal Angular app, monoliths are simpler and Module Federation adds real overhead for no benefit at that scale. It starts paying off once you have multiple teams, independent release cycles, or a large legacy app you want to modernize piece by piece instead of all at once.

Wrapping up

Micro frontends are not magic, they are really just "load this other app's code at runtime instead of bundling it together." Once that clicks, the actual Angular setup is mostly configuration, not something conceptually hard.

Top comments (0)