You’ve Been Building Angular Apps Wrong!
Break Free from Monolithic Limits: Build Better with Microfrontends in Angular
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.
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.
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:
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:
- Payment
- 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:
- Account Number
- Amount Text Fields
- 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:
- Search input
- 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 remotes
field. 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.