This tutorial shows how to use Webpack Module Federation together with the Angular CLI and the @angular-architects/module-federation plugin. The goal is to make a shell capable of loading a separately compiled and deployed microfrontend:
Important: This tutorial is written for Angular and Angular CLI 14 and higher. To find out about the small differences for lower versions of Angular and for the migration from such a lower version, please have a look to our migration guide.
In this part you will clone the starter kit and inspect its projects.
-
Clone the starter kit for this tutorial:
git clone https://github.com/manfredsteyer/module-federation-plugin-example.git --branch starter -
Move into the project directory and install the dependencies with npm:
cd module-federation-plugin-example npm i -
Start the shell (
ng serve shell -o) and inspect it a bit:-
Click on the
flightslink. It leads to a dummy route. This route will later be used for loading the separately compiled Micro Frontend. -
Have a look to the shell's source code.
-
Stop the CLI (
CTRL+C).
-
-
Do the same for the Micro Frontend. In this project, it's called
mfe1(Micro Frontend 1) You can start it withng serve mfe1 -o.
Now, let's activate and configure module federation:
-
Install
@angular-architects/module-federationinto the shell:ng add @angular-architects/module-federation --project shell --type host --port 4200Also, install it into the micro frontend:
ng add @angular-architects/module-federation --project mfe1 --type remote --port 4201This activates module federation, assigns a port for ng serve, and generates the skeleton of a module federation configuration.
-
Switch into the project
mfe1and open the generated configuration fileprojects\mfe1\webpack.config.js. It contains the module federation configuration formfe1. Adjust it as follows:const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack'); module.exports = withModuleFederationPlugin({ name: 'mfe1', exposes: { // Update this whole line (both, left and right part): './Module': './projects/mfe1/src/app/flights/flights.module.ts', }, shared: { ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto', }), }, });
This exposes the
FlightsModuleunder the Name./Module. Hence, the shell can use this path to load it. -
Switch into the
shellproject and open the fileprojects\shell\webpack.config.js. Make sure, the mapping in the remotes section uses port4201(and hence, points to the Micro Frontend):const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack'); module.exports = withModuleFederationPlugin({ remotes: { // Check this line. Is port 4201 configured? mfe1: 'http://localhost:4201/remoteEntry.js', }, shared: { ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto', }), }, });
This references the separately compiled and deployed
mfe1project. -
Open the
shell's router config (projects\shell\src\app\app.routes.ts) and add a route loading the Micro Frontend:{ path: 'flights', loadChildren: () => import('mfe1/Module') .then(m => m.FlightsModule) }, { path: '**', component: NotFoundComponent } // DO NOT insert routes after this one. // { path:'**', ...} needs to be the LAST one.
Please note that the imported URL consists of the names defined in the configuration files above.
-
As the URL
mfe1/Moduledoes not exist at compile time, ease the TypeScript compiler by adding the following line to the fileprojects\shell\src\decl.d.ts:declare module 'mfe1/Module';
Now, let's try it out!
-
Start the
shellandmfe1side by side in two different terminals:ng serve shell -o ng serve mfe1 -oHint: You might use two terminals for this.
-
After a browser window with the shell opened (
http://localhost:4200), click onFlights. This should load the Micro Frontend into the shell: -
Also, ensure yourself that the Micro Frontend also runs in standalone mode at http://localhost:4201:
Hint: You can also call the following script to start all projects at once: npm run run:all. This script is added by the Module Federation plugin.
Congratulations! You've implemented your first Module Federation project with Angular!
Now, let's remove the need for registering the Micro Frontends upfront with with shell.
-
Switch to your
shellapplication and open the fileprojects\shell\webpack.config.js. Here, remove the registered remotes:remotes: { // Remove this line: // "mfe1": "http://localhost:4201/remoteEntry.js", },
-
Open the file
app.routes.tsand use the functionloadRemoteModuleinstead of the dynamicimportstatement:import { loadRemoteModule } from '@angular-architects/module-federation'; [...] const routes: Routes = [ [...] { path: 'flights', loadChildren: () => loadRemoteModule({ type: 'module', remoteEntry: 'http://localhost:4201/remoteEntry.js', exposedModule: './Module' }) .then(m => m.FlightsModule) }, [...] ]
Remarks:
type: 'module'is needed for Angular 13 or higher as beginning with version 13, the CLI emits EcmaScript modules instead of "plain old" JavaScript files. -
Restart both, the
shelland the micro frontend (mfe1). -
The shell should still be able to load the micro frontend. However, now it's loaded dynamically.
This was quite easy, wasn't it? However, we can improve this solution a bit. Ideally, we load the Micro Frontend's remoteEntry.js before Angular bootstraps. This file contains meta data about the Micro Frontend, esp. about its shared dependencies. Knowing about them upfront helps Module Federation to avoid version conflicts.
- Switch to the
shellproject and open the filemain.ts. Adjust it as follows:
import { loadRemoteEntry } from '@angular-architects/module-federation';
Promise.all([
loadRemoteEntry({
type: 'module',
remoteEntry: 'http://localhost:4201/remoteEntry.js',
}),
])
.catch((err) => console.error('Error loading remote entries', err))
.then(() => import('./bootstrap'))
.catch((err) => console.error(err));-
Restart both, the
shelland the micro frontend (mfe1). -
The shell should still be able to load the micro frontend.
So far, we just hardcoded the URLs pointing to our Micro Frontends. However, in a real world scenario, we would rather get this information at runtime from a config file or a registry service. This is what this part of the lab is about.
- Switch to the shell and create a file
mf.manifest.jsonin itsassetsfolder (projects\shell\src\assets\mf.manifest.json):
{
"mfe1": "http://localhost:4201/remoteEntry.js"
}- Adjust the shell's
main.ts(projects/shell/src/main.ts) as follows:
import { loadManifest } from '@angular-architects/module-federation';
loadManifest('assets/mf.manifest.json')
.catch((err) => console.error('Error loading remote entries', err))
.then(() => import('./bootstrap'))
.catch((err) => console.error(err));The imported loadManifest function also loads the remote entry points.
- Adjust the shell's lazy route pointing to the Micro Frontend as follows (
projects/shell/src/app/app.routes.ts):
{
path: 'flights',
loadChildren: () =>
loadRemoteModule({
type: 'manifest',
remoteName: 'mfe1',
exposedModule: './Module'
})
.then(m => m.FlightsModule)
},-
Restart both, the
shelland the micro frontend (mfe1). -
The shell should still be able to load the micro frontend.
Hint: The ng add command used initially also provides an option --type dynamic-host. This makes ng add to generate the mf.manifest.json as well as the call to loadManifest in the main.ts.
- Add a library to your monorepo:
ng g lib auth-lib
- In your
tsconfig.jsonin the workspace's root, adjust the path mapping forauth-libso that it points to the libs entry point:
"auth-lib": [
"projects/auth-lib/src/public-api.ts"
]-
As most IDEs only read global configuration files like the
tsconfig.jsononce, restart your IDE (Alternatively, your IDE might also provide an option for reloading these settings). -
Switch to your
auth-libproject and open the fileauth-lib.service.ts(projects\auth-lib\src\lib\auth-lib.service.ts). Adjust it as follows:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class AuthLibService {
private userName: string;
public get user(): string {
return this.userName;
}
constructor() {}
public login(userName: string, password: string): void {
// Authentication for **honest** users TM. (c) Manfred Steyer
this.userName = userName;
}
}- Switch to your
shellproject and open itsapp.component.ts(projects\shell\src\app\app.component.ts). Use theAuthLibServiceto login a user:
[...]
// IMPORTANT: Make sure you import the service
// from 'auth-lib'!
import { AuthLibService } from 'auth-lib';
[...]
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {
title = 'shell';
constructor(private service: AuthLibService) {
this.service.login('Max', null);
}
}- Switch to your
mfe1project and open itsflights-search.component.ts(projects\mfe1\src\app\flights\flights-search\flights-search.component.ts). Use the shared service to retrieve the current user's name:
[...]
// IMPORTANT: Make sure you import the service
// from 'auth-lib'!
import { AuthLibService } from 'auth-lib';
[...]
export class FlightsSearchComponent {
// Add this:
user = this.service.user;
// And add that:
constructor(private service: AuthLibService) { }
[...]
}- Open this component's template(
flights-search.component.html) and data bind the propertyuser:
<div id="container">
<!-- Add this line: -->
<div>User: {{user}}</div>
[...]
</div>-
Restart both, the
shelland the micro frontend (mfe1). -
In the shell, navigate to the micro frontend. If it shows the used user name
Max, the library is shared.
Remarks: All the libraries of your Monorepo are shared by default. The next section shows how to select libraries to share.
So far, all dependencies have been shared. The used shareAll function makes sure, all packages in your package.json's dependencies section are shared and by default, all monorepo-internal libraries like the auth-lib are shared too.
While this makes getting started with Module Federation easy, we can get a more performant solution by directly defining what to share. This is because shared dependencies are not tree-shakable and they end up in a bundle of their on that needs to be loaded.
For explicitly sharing our dependencies, you could switch to the following configurations:
- Shell's
webpack.config.js(projects\shell\webpack.config.js):
// Import share instead of shareAll:
const { share, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
remotes: {
// Check this line. Is port 4201 configured?
// "mfe1": "http://localhost:4201/remoteEntry.js",
},
// Explicitly share packages:
shared: share({
'@angular/core': {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
},
'@angular/common': {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
},
'@angular/common/http': {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
},
'@angular/router': {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
},
}),
// Explicitly share mono-repo libs:
sharedMappings: ['auth-lib'],
});- Micro Frontend's
webpack.config.js(projects\mfe1\webpack.config.js):
// Import share instead of shareAll:
const { share, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'mfe1',
exposes: {
// Update this whole line (both, left and right part):
'./Module': './projects/mfe1/src/app/flights/flights.module.ts',
},
// Explicitly share packages:
shared: share({
'@angular/core': {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
},
'@angular/common': {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
},
'@angular/common/http': {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
},
'@angular/router': {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
},
}),
// Explicitly share mono-repo libs:
sharedMappings: ['auth-lib'],
});After that, restart the shell and the Micro Frontend.
Have a look at this article series about Module Federation


