How to add Module Federation into your Angular micro-frontend apps

Michael De Abreu - Oct 18 '23 - - Dev Community

Photo by Arseniy Kapran on Unsplash

Hi there! I am very grateful for the overwhelming reception that my first post in this series got. At first, I thought you would like to know about sharing styles between your apps, or maybe how to integrate the dependencies better, but instead, and by far, the most requested feature was how to add Module Federation into my app. Even some of you are saying that all the work without MF is not updating into a micro-frontend, but into apps that become libraries, and you would be right actually.

Nevertheless, I mentioned Module Federation at the beginning of the article. I pointed out that if you want to start a project with micro-frontend in mind upfront, you should use NX instead of plain Angular, because it has a great system to integrate MF, among other features.

This doesn't mean we can integrate Module Federation with plain Angular, and I think the state we got in my last post is the perfect state to do so.

Knowing our tools 🛠️

For this, we are going to use the @angular-architects/module-federation library. This library has been developed to be integrated with Angular CLI, enabling the Module Federation integration with very little configuration on our end.

Keep in mind that currently, Angular CLI doesn't provide a way to load custom Webpack configuration, so this will update the Angular CLI builder with a custom builder that does allow extending the Webpack config file. You can keep that custom builder or replace it with another if you know how to do it, but I won't talk about it here, so we will trust this library.

What does the library do? 📖

As I mentioned, the library first is going to update our build configuration to allow us to extend the Webpack config file, with a file that we are going to fill to enable the Federation Module using the functions this library provides.

The library also allows us to serve the applications individually, so we can interact with the application normally through the browser, or as a micro-frontend.

There are several alternatives to integrate this library into our apps, including static and dynamic hosting. The library provides schematics to set up both the remote app and the host, but in my tests no matter what you choose, you will need to update some things yourself.

Setting up the Module Federation in the main app 🎤

As I mentioned, we are going to use the library's schematics to initialize the configuration, so, open your terminal at the root of the project, and use the schematics as:

ng add @angular-architects/module-federation --project app-micro --type dynamic-host --port 4200
Enter fullscreen mode Exit fullscreen mode

This will configure the main project, update the Angular CLI config with the new builders, create a manifest file with the information to load the remote entry of our secondary projects, and also, will split our bootstrapping into two files, once for the actual bootstrap, and one to allow Angular CLI to handle the bootstrapping.

This most likely won't affect you, but some schematics (e.g.: @angular/pwa and @angular/material) expect the bootstrapping process to happen in the main.ts file. If you want to add one of these libraries you can temporarily disable this.

ng g @angular-architects/module-federation:boot-async false -- project {{ YOUR MAIN PROJECT }}

ng add {{ your-libraries-of-chioce }} --project yourProject

ng g @angular-architects/module-federation:boot-async true -- project {{ YOUR MAIN PROJECT }}

If you check the changes, the manifest file mf.manifest.json expects a file to be served in port 4200, but that's the port we use for the main app. Additionally, if you check the app-routing.module.ts file, you can see that we are loading the secondary app as a library, just as we did before this change. So, after the schematic has run, we still going to need to do some updates in our code.

First, open the manifest file and update the port accordingly. In this case, we are going to set up the secondary app with the port 4201, so you should end up with something like this:

{
  "login": "http://localhost:4201/remoteEntry.js"
}
Enter fullscreen mode Exit fullscreen mode

For this example, we are keeping things simple, by having this information statically in a JSON file, but in a real-life application, this should be served dynamically, replacing the JSON with a prod version, or you can set up an endpoint in your server returning the manifest configuration. If you choose the latter, you should update the main.ts, to change the URL that the initFederation function is using to get the information. Keep in mind that if you configure it this way, the application won't be able to load at all if there is no internet connection, but if you set up a static replace, you can handle the connection error in case your app has been cached previously.

After we have updated the manifest file accordingly, we'll need to use the manifest file in our routing configuration to load the secondary app. Open the app-rounting.module.ts file, remove the import('@@login') code, and replace it with a call of the loadRemoteModule function (import the function from the @angular-architects/module-federation module). You need to pass a configuration object to the function, and we are going to use this one:

{
  type: 'manifest',
  remoteName: 'login',
  exposedModule: './Module',
}
Enter fullscreen mode Exit fullscreen mode
  • type: The type of remote configuration we are using. In this case, we are using 'manifest' as we are loading the configuration with a manifest file, but you could load the module directly.
  • remoteName: The name of the module that we are going to load, as it is being declared as the key in the manifest file.
  • exposedModule: The route of the module we are loading. This is the only thing that's defined in the secondary app.

All of this is configurable, but these are the values we are going to use in this example.

You should end up with something like this:

...
const routes: Routes = [
... 
  {
    path: '',
    canMatch: [isNotLogged],
    loadChildren: () =>
      loadRemoteModule({ type: 'manifest', remoteName: 'login', exposedModule: './Module' }).then(
        (m) => m.LoginModule,
      ),
  },
...
Enter fullscreen mode Exit fullscreen mode

With all this, we would have configured the main app to be able to load the other apps through a remote configuration. Now we need to configure the secondary apps we want to be loaded this way.

Configuring the secondary apps remote config ⏏️

Just as we did with the main app, we are going to use the schematics that are provided with the package. In this case, instead of using --type dynamic-host, we are going to use --type remote instead.

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

And, just as this did with the main app, it will update several parts of the project, but it won't create a manifest file, and if you check the webpack.config.js file, you can see it has more content than the one that the main app is using

Specifically, it has an exposes property, with an object configuration, with a key that looks like a route and a value that looks like another route. If you remember earlier, when we were configuring the route, we used the exposedModule property, with a value that looks like a route.

The exposes property configures how and what should the module expose to the loader, and with the exposedModule property we access that configuration to load what we need. However, you can notice that we currently are trying to load a './Module' in the route configuration, but this Webpack configuration doesn't have that property.

We need to update that. Remove the './Component' key, and add the './Module' key with a relative path to load the app module. It should look like this now:

...
  exposes: {
    './Module': './projects/login/src/app/feature/login/login.module.ts',
  },
...
Enter fullscreen mode Exit fullscreen mode

That's all the update we need to do in the secondary app.

Running it 🎭

Now, let's test this.

First, run only the main app

ng serve
Enter fullscreen mode Exit fullscreen mode

If you try to access the application, because we are not serving our login app, the load of that module is going to fail, and the application is going to go directly to our dashboard. (Yes, very secure). After verifying that the login part is not loading, we serve the login app.

ng serve login
Enter fullscreen mode Exit fullscreen mode

Now, that we have served the login app, we need to refresh the main application to watch the changes. You will see our login app being correctly mounted into our main app.

Additionally, if you open the browser with the localhost:4201 you will see the login app running independently.

What's next? 🔮

After this, we could improve the build configuration, as currently, we are using the shareAll function of the '@angular-architects/module-federation/webpack package to share all the dependencies. However, we should only share the dependencies we really need to share, using the share function, and configuring each library as needed.

That's all folks! 🐇

We have successfully served our application using the Module Federation feature. I know this took longer than expected, but some exciting things happened in my life, and I needed some time to adjust myself.

I really hope you enjoyed this, and I hope I can write more about this topic, so just as before, what do you think I should talk about next? I was thinking maybe we could integrate two different apps within a shell-like app, so instead of moving from one big app to several micro apps, move from several apps and integrate them with a micro app. But let me know in the comments what you think.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .