You’ve Been Building Angular Apps Wrong!

Break Free from Monolithic Limits: Build Better with Microfrontends in Angular

Thamodi Wickramasinghe
Bits and Pieces

--

Introduction

Instead of complex web applications, most organizations tend to go for microfrontends that lets them break free from building giant monoliths.

Simply put, microfrontends follow the same structure as microservices.

And, one of the widely accepts approaches on working with microfrontends is through Module Federation that’s offered by Webpack5.

So, let’s look closer at how you can create and manage microfrontends with Angular using Module Federation.

What is Microfrontends?

Before microfrontends, web applications were treated and built as a giant monolith. This means that your all of your frontend components were treated as a single application, as shown below.

Figure: A Monolith Architecture

But this was problematic…

By following this approach, this meant that a single change to a part of your app needed a full deployment of your entire app. This was extremely time consuming and made the overall release workflow unproductive.

Therefore, to overcome these issues, microservices were introduced.

Figure: A Microservices Architecture

With microservices, teams were able to breakdown the monolithic backend onto smaller components that were designed, built and deployed in isolated and by a single team.

This meant that you could deploy one service without impacting the entire app. But, the frontend was still the same. If you made a change in your frontend app to update a single API endpoint, you’d have to re-deploy your entire app. So, ultimately, your frontend app was still complex, hard to maintain and was also acting as a single point of failure.

This is where microfrontends came into the picture…

A microfrontend architecture aims to bring the same principle as a microservice on to a frontend. Simply put, it breaks down your giant monolith frontend onto smaller apps that are again designed, developed and deployed by independent teams.

By introducing such an approach, your web application can leverage concepts like lazy loading and boost its performance, while focusing on addressing key developer centric issues such as better maintainiabiliy and faster release times.

For example, consider the reference architecture attached below:

Figure: A Reference Microfrontend App

As shown above, we’ve introduced multiple frontends to manage different parts of our app, rather than building it inside one giant app. In this instance, we’ve introduced three apps — User List, Notifications and Blog List frontend apps to seperate each part of the business domain.

But you might wonder, how do we connect all of these apps together?

Well, there are several ways to integrate these microfrontends — the most common being “Module Federation.”

What is Module Federation?

WebPack5 introduced Module Federation to integrate multiple microfrontends. Using Module Federation, developers can integrate independent applications onto a giant single app which are loaded through lazy loading. So, this approach not only improves developement productivity, but it also boosts the overall performance of the app.

So, let’s get our hands dirty and create a microfrontend app with Module Federation!

Phase 01: Setting Up the Angular Project

Let’s build a payment-based application with Angular and Module Federation. Our app will comprise of two main components:

  1. Payment
  2. Billing

Therefore, let’s first create two separate applications for Payment and Billing.

Step 01 - Application Creation

To create two new applications, use the command:

ng new payment
ng new billing

Step 02 - Create New Components and Create the UI

Next, let’s build each application in isolation by adding some components onto our microfrontend

Building the Payment Application

Let’s create a payment component in our payment app using the command:

ng generate component payment

As shown below, the payment microfrontend will expose the payment component that comprises consisting of:

  1. Account Number
  2. Amount Text Fields
  3. Pay Now Button.

Building the Billing Application

Next, let’s add the billing information component onto our billing app:

ng generate component billing

The billing microfrontend will expose the billing component, which consists of:

  1. Search input
  2. Table to display all the payments.

Step 03 - Create routes

Next, it’s important to define a route in both of our applications that tell the Angular router of each application on which component to load.

Since we’re working with microfrontends, we will want to load the components that we created as the root path of each microfrontend app. This is done because when we integrate the app finally, we will be using path based routing such as /billing and /payment to launch each app.

And once we launch each app, we will load the base path of each app, that results in the below routing confugration to be applied:

Routing Configuration for the Payment Application

export const APP_ROUTES: Routes = [
{ path: '', component: PaymentComponent },
];

Routing Configuration for the Billing Application

export const APP_ROUTES: Routes = [
{ path: '', component: BillingComponent },
];

Step 04 - Expose modules to be used by other applications

Next, we have to export the modules from our microfrontends that we aim on using across our main application. So let’s go ahead and export the components that we just created:

Payment Application

@NgModule({
imports: [
CommonModule,
RouterModule.forChild(APP_ROUTES),
PaymentComponent
]
})
export class PaymentModule { }

Billing Application

@NgModule({
imports: [
CommonModule,
RouterModule.forChild(APP_ROUTES),
BillingComponent
]
})
export class BillingModule { }

Hereafter, let’s try to integrate these two microfrontends using module federation.

Configuring Module Federation in Angular

To introduce Module Federation, we must create a shell application to host the two microfrontends.

If you’re not familiar with a shell app, it’s essentially a way that you can render part of your application using a route at build time

This is helpful in our case as we want to render the payment and billing components based on declerative routing. So, let’s look at how we can create a shell application and integrate the microfrontends into the shell application.

Step 01 - Shell Application Creation

Now, keep in mind that a shell application is an Angular app on its own. So, let’s go ahead and create a new Angular app to host our shell:

ng new shell

Step 02 - Install Module Federation in the application

Next, we will need to install the Module Federation module that is offered by Angular to start integrating our microfrontends onto our shell.

To do so, run the command below to install the module onto your shell app:

ng add @angular-architects/module-federation

Step 03 - Configure Module Federation in the Applications

Next, we’ll need to expose a path to the modules of our microfrontend apps (that we edited earlier) in the webpack.config.js.

Payment Application

const { ModuleFederationPlugin } = require('webpack').container.ModuleFederationPlugin;

module.exports = withModuleFederationPlugin({
name: 'payment',

exposes: {
'./Module': './payment/src/app/payment/payment.module.ts',
},

shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});

In the snippet above, we have defined the name of the microfrontend in the name field. The exposes property contains the module we will be exposing to other applications. We need to state the exact path of the module in the exposes object.

Let’s also repeat configuring the webpack.config.js file in the other microfrontend.

Billing Application

const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin;

module.exports = withModuleFederationPlugin({
name: 'billing',
exposes: {
'./Module': './billing/src/app/billing/billing.module.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},

});

Next, we must configure the two modules we are integrating into the Shell application.

Shell Application

plugins: [
new ModuleFederationPlugin({
library: { type: "module" },
name: "shell",
remotes: {
payment: 'payment@http://localhost:4201/remoteEntry.js',
billing: 'billing@http://localhost:4202/remoteEntry.js',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
}),
sharedMappings.getPlugin()
],

We define the microfrontends that we plan to integrate into the remotesfield. In this example, we have integrated Payment and Billing microfrontends. We must include the path of the remoteEntry.js file, which the webpack will generate.

Step 04 — Create routes for the shell application

Next, let’s define the routes that we will be launching in the shell application to help load each microfronted. Go ahead and update your routing configuration on your Shell App as shown below:

const routes: Routes = [
{
path: 'payment',
loadChildren: () => import('../../../payment/src/app/payment/payment.module').then(m => m.PaymentModule)
},
{
path: 'billing',
loadChildren: () => import('../../../billing/src/app/billing/billing.module').then(m => m.BillingModule)
},
];

@NgModule({
imports: [RouterModule.forRoot(routes, {})],
exports: [RouterModule],
})
export class AppRoutingModule { }

Here, we are defining two routes to integrate the two applications. We will be defining the route in the path field and loadChildren field consists of the module information we are integrating.

In the loadChildren field, we need to declare the exact path of the module we will be integrating. In this example, we have declared the path for the payment module in the payment application.

loadChildren: () => import('../../../payment/src/app/payment/payment.module').then(m => m.PaymentModule)

We can verify the implementation after defining the routes in the shell application. As you can say, we’re implementing lazy loading that can improve load times as well.

Step 05 — Verify the implementation

Next, let’s run our apps and see if it works as expected. First, execute the below commands to run the microfrontends.

ng serve --project=payment --port=4201
ng serve --project=payment --port=4202

Next, execute the below command to run the shell application.

ng serve --project=shell --port=4200

Visit http://localhost:4200 in your browser to see the integrated microfrontends within the Shell application.

Based on the above image, the shell application is running. According to the routes defined earlier, we can lazy load the integrated applications.

Visit http://localhost:4200/payment in your browser to see the integrated Payment microfrontend within the Shell application.

Visit http://localhost:4200/billing in your browser to see the integrated Billing microfrontend within the Shell application.

If you have followed the above example correctly, Good Job!

What are the best practices of Module Federation with Angular?

Now that we have a basic understanding of creating microfrontends and integrating them with Module Federation, let’s look at how we can take it a step further to improve your application’s performance by following the best practices and guidelines below.

Practise 01: Clear Module Boundaries

Ensure that each microfrontend module has a clear boundary set to make the integration seamless. Having clear boundaries ensures encapsulation while making it easy to maintain. Furthermore, avoiding tight coupling between Microfrontends is better to maintain independence.

This can be achieved by using Domain Driven Design.

Practise 02: Shared Libraries and Dependencies

To reduce duplication, identify the common functionalities and make it a shareable module. This can be achieved by building your components and microfrontends using revolutionising build systems like Bit.

As you can see, tools like Bit keep track of all the dependencies that’s being used behind the scenes. So, once you update a child, it automatically updates the components that’s being used across the tree.

Practise 03: Versioning and Compatibility

While integrating multiple microfrontends, we must avoid version conflicts where we can expect smooth updates. Furthermore, it is required to implement versioning strategies for shared modules.

Practise 04: Error Handling, Testing, and Monitoring

Error handling is crucial while integrating microfrontends; errors in one microfrontend should not affect another. Therefore, it is required to implement error boundaries or isolation techniques.

To avoid errors in the created microfrontends, each must undergo excessive test cycles. Furthermore, every microfrontend must undergo unit testing and integration tests.

We can also include monitoring tools for all the microfrontends while having proper logging and debugging mechanisms to identify issues.

Wrapping Up

Integrating microfrontends with Angular and Module Federation marks a paradigm shift in frontend development. We can create dynamic, adaptive, modular, scalable, and a future-proof system by adapting microfrontend architecture and Module Federation.

If you wish to explore the app that we built in this article, feel free to check out it’s code!

I hope that you have found this helpful.

Thank you for reading.

--

--