Miloš Zeljko

Microfrontends Architecture in Angular

Miloš Zeljko
April 23, 2023 12 mins to read
Share

What is Microfrontends Architecture?

Microfrontends architecture is an innovative approach to developing modern web applications by breaking them down into smaller, more manageable pieces. The concept builds upon the success of microservices, which have revolutionized the way backend systems are built and maintained.

In a microfrontends architecture, each microfrontend corresponds to a specific feature or a set of closely related features in the application. These microfrontends can be developed, tested, and deployed independently, allowing teams to work on different application parts without affecting each other. This separation of concerns helps to improve the overall development process, making it easier to maintain and evolve the application over time.

Benefits of Microfrontends Architecture

There are several advantages to adopting a microfrontends architecture for your Angular applications:

  1. Scalability: Microfrontends allow for easier application scaling, as individual components can be updated and deployed independently.
  2. Faster development cycles: Teams can work on different microfrontends simultaneously, reducing the time required to develop new features or make changes.
  3. Improved code quality: Smaller, more focused codebases are easier to maintain and test, leading to higher code quality and fewer bugs.
  4. Greater flexibility: Microfrontends enable you to use different technologies, libraries, and frameworks within the same application, allowing you to choose the best tools for each component.
  5. Easier onboarding: New team members can quickly become familiar with a specific microfrontend, instead of needing to learn the entire codebase.

Angular and Microfrontends

Why Angular for Microfrontends Architecture?

Angular is a popular, powerful, and versatile web development framework, making it an excellent choice for implementing microfrontends architecture. Its component-based structure naturally aligns with the idea of dividing an application into smaller, more manageable pieces. Here are some reasons why Angular is well-suited for microfrontends:

  1. Modular Structure: Angular's modular design, which relies on NgModule, makes it easy to create and manage self-contained features within an application.
  2. Component-based: Angular's component-based architecture provides a natural foundation for developing isolated and reusable UI elements that can be easily integrated into a microfrontend.
  3. Dependency Injection: Angular's built-in dependency injection system allows for better separation of concerns, making it easier to manage and test individual microfrontends.
  4. Robust Ecosystem: Angular's mature ecosystem, with a vast array of libraries, tools, and resources, supports the implementation of microfrontends, making it a reliable choice for such projects.
  5. Strong Community: Angular has a large and active community of developers, who are constantly contributing to the framework’s improvement and creating new tools and libraries that can be leveraged in microfrontend projects.

Angular Version and Compatibility

Before diving into implementing microfrontends architecture with Angular, you must ensure that you are using a compatible Angular version. The introduction of Module Federation in Webpack 5 has made it much simpler to implement microfrontends. Therefore, it is recommended to use Angular 12 or later, which offers official support for Webpack 5 and Module Federation.

In the next chapters, we will explore how to use Module Federation and Nx Workspace to implement a microfrontends architecture in Angular using a monorepo strategy, covering the setup, configuration, and integration of these tools to achieve a seamless development experience.

Module Federation

Understanding Module Federation

Module Federation is a feature introduced in Webpack 5 that allows for dynamic code sharing between independent applications at runtime. It enables the seamless integration of separate builds, reducing the need for manual coordination and improving the overall efficiency of the development process.

In a microfrontends architecture, Module Federation makes it possible to share and reuse components, libraries, and services across different microfrontends. It allows each microfrontend to be developed, built, and deployed independently, while still being able to access shared resources from other microfrontends or a host application.

Advantages of Module Federation in Angular

Using Module Federation with Angular offers several benefits:

  1. Simplified Code Sharing: Module Federation makes it easy to share code between microfrontends, reducing the need for duplication and improving maintainability.
  2. Lazy Loading: Module Federation supports lazy loading, allowing you to optimize the loading of shared resources and improve the performance of your application.
  3. Versioning and Dependency Management: Module Federation allows for separate versioning and dependency management for each microfrontend, giving you more control over the dependencies used in your application.
  4. Faster Development: By enabling dynamic integration of microfrontends, Module Federation can speed up the development process and make it easier to work on different application parts simultaneously.

Nx Workspace

What is Nx Workspace?

Nx Workspace is a powerful and extensible dev tools platform developed by Nrwl. It provides a unified workspace to manage and scale your Angular applications, libraries, and other shared resources. Nx Workspace is built on top of Angular CLI, enhancing its capabilities and adding a set of advanced features tailored for large-scale and complex projects.

Using Nx Workspace, developers can create and manage multiple Angular applications and libraries within a single workspace, making it easier to share code, enforce best practices, and streamline the development process. Nx Workspace is especially beneficial for implementing a microfrontends architecture, as it provides a unified structure to manage the different microfrontends and their dependencies.

Benefits of Using Nx Workspace with Angular

Here are some key benefits of using Nx Workspace for your Angular microfrontend projects:

  1. Monorepo Management: Nx Workspace enables you to manage multiple Angular applications and libraries within a single repository, facilitating code sharing and collaboration across projects.
  2. Efficient Build System: Nx Workspace provides an advanced build system that leverages caching and incremental builds to speed up the development process.
  3. Code Sharing: With Nx Workspace, you can easily create shared libraries and components that can be reused across multiple microfrontends, promoting consistency and reducing code duplication.
  4. Extensibility: Nx Workspace can be extended with various plugins and community-built tools, providing a flexible ecosystem to cater to your project’s specific needs.
  5. Enhanced Developer Experience: Nx Workspace offers a set of advanced tools and features, such as dependency graph visualization, code generation, and linting, to improve the overall developer experience.

Integrating Microfrontends with Angular, Module Federation, and Nx Workspace

Install Angular CLI and Nx

We must have the Angular CLI, nrwl schematics and NX installed globally. Run the following commands:

npm install -g @angular/cli
npm install -g @nrwl/schematics
npm install --global nx@latest

Create a new Nx workspace

Run the below command in a terminal to make a new NX workspace:

npx create-nx-workspace@latest my-workspace

You’ll be prompted for a preset. Select Integrated Monorepo then Angular.

Other options are irrelevant as we will delete the Angular app that comes generated with the workspace.

Create a host application

Change directories into the newly created workspace:

cd ./my-workspace

From /apps delete the app-name and app-name-e2e folders of the app that came with the workspace template.

Install the Angular schematic needed to create an Angular app:

npm install @nrwl/angular

Create a new host application by running the below command in the terminal:

npx nx g @nrwl/angular:host shell --dynamic

@nrwl/angular:host – This will select a template with module federation configured for the host application.

shell – Host application name.

--dynamic – Tell NX to create an app with dynamic module federation.

More information can be found here.

This application will be used as a “shell”. Its primary role is to glue microfrontends together. It will handle routing towards independent microfrontends and will define some shared dependencies between our applications.

Create other microfrontend applications

Create as many new apps as you want by running the below command in the terminal:

npx nx g @nrwl/angular:remote remote1 --host=shell

@nrwl/angular:remote – This will select a template with module federation configured for remote application.

remote1 – Remote application name.

--host=shell – This will tell NX to add this application to the shell application as remote.

More information can be found here.

These applications will be our microfrontends.

Create libraries for shared code between shell and remotes

In order to share code between our microfrontends, we can create a library and place code there. As each microfrontend runs independently it won’t be able to reference code from another microfrontend directly. We can use the library locally inside monorepo or publish it to a package registry like npm.

To create a library run the following command:

nx g @nrwl/angular:lib library-name

Don’t forget to add new exports in "index.ts" inside the library for each new class you want to be accessible from outside of the library:

export * from './lib/my-component.component';
export * from './lib/my-service.service';

In order to use classes from the library you can import them where needed with the import:

import { MyComponentComponent} from '@my-workspace/library-name';
import { MyServiceService } from '@my-workspace/library-name';

Run the application

To run our application run the following command in the terminal:

nx serve shell

shell – Name of the host application that glues our microfrontend applications.

--devRemotes="remote1" – Provide a list of microfrontend names that are part of the shell application, separated with commas. For example: –devRemotes=”remote1,remote2,remote3″

If you open http://localhost:4200 you can see the shell application running.

microfrontends 1

You can navigate to the remote1 application by using the navbar on the left side. If you look at the route you will see http://localhost:4200/remote1. That means that the remote1 application is running inside our shell application!

microfrontends 2

If we navigate to http://localhost:4201 we can see the remote1 application running independently on its own port.

microfrontends 3

How does Module Federation work?

Let us now inspect some of the files generated by NX to get a better understanding of how module federation works.

Shell application

Firstly, let us take a look at the shell application, inside apps/shell/src/app.

Inside remotes.d.ts we can see the declaration of remote1/Module.

declare module 'remote1/Module';
remotes.d.ts

This tells the typescript compiler that remote1/Module is a known element. As we are loading remote1 code during runtime, the typescript compiler doesn’t know of any source code from the remote1 application as it gets loaded from another location. This way we tell the compiler to resolve this name as known as we guarantee for it to be available at runtime.

Inside webpack.config.js we can see the configuration that sets module-federation.config.js as a place where module federation is being configured.

const { withModuleFederation } = require('@nrwl/angular/module-federation');
const config = require('./module-federation.config');
module.exports = withModuleFederation(config);
webpack.config.js

Inside module-federation.config.js we define the name of the module federation module, remotes, and dependencies we want to share between the host and remotes.

module.exports = {
  name: 'shell',
  remotes: [],
};
module-federation.config.js

In this example, we are defining module names shell with no remotes. But how do we get the remote1 application running inside the shell application then? Inside main.ts we are defining the path to module-federation.manifest.json which contains a list of remote modules with the URL pointing to them. This list gets loaded as remotes during compilation, so we don’t need to add them manually in the module-federation.config.js

import { setRemoteDefinitions } from '@nrwl/angular/mf';

fetch('/assets/module-federation.manifest.json')
  .then((res) => res.json())
  .then((definitions) => setRemoteDefinitions(definitions))
  .then(() => import('./bootstrap').catch((err) => console.error(err)));
main.ts
{
  "remote1": "http://localhost:4201"
}
module-federation.manifest.json

Lastly, inside app/app.routes.ts we are binding certain microfrontend with a route.

import { NxWelcomeComponent } from './nx-welcome.component';
import { Route } from '@angular/router';
import { loadRemoteModule } from '@nrwl/angular/mf';

export const appRoutes: Route[] = [
  {
    path: 'remote1',
    loadChildren: () =>
      loadRemoteModule('remote1', './Module').then((m) => m.RemoteEntryModule),
  },
  {
    path: '',
    component: NxWelcomeComponent,
  },
];
app.routes.ts

Remote1 application

The main difference between the shell and remote applications is in the module-federation.config.js file. Instead of defining remotes, we are defining exposed modules. RemoteEntryModule serves as an entry point from the host to the remote application.

module.exports = {
  name: 'remote1',
  exposes: {
    './Module': 'apps/remote1/src/app/remote-entry/entry.module.ts',
  },
};
module-federation.config.js

From there we can define routing inside a remote module in entry.routes.ts.

import { Route } from '@angular/router';
import { RemoteEntryComponent } from './entry.component';

export const remoteRoutes: Route[] = [
  { path: '', component: RemoteEntryComponent },
];
entry.routes.ts

Best Practices and Recommendations

Maintaining Consistency Across Microfrontends

To maintain consistency across microfrontends, follow these practices:

  • Create shared libraries for common components, styles, and utilities to ensure a consistent look and feel across all microfrontends.
  • Establish and enforce coding standards and best practices across all teams working on the project.
  • Use a design system or a UI component library to ensure consistent UI components and user experiences.
  • Document and communicate any changes or updates to shared resources, to keep all teams in sync.

Avoiding Common Pitfalls

Here are some common pitfalls to avoid when implementing a microfrontends architecture:

  • Over-engineering: Be cautious about overcomplicating the architecture. Evaluate the needs of your project and choose the right level of complexity.
  • Excessive communication between microfrontends: Minimize the amount of interdependence between microfrontends to prevent performance issues and tight coupling.
  • Duplicated code: Leverage shared libraries to reduce code duplication and promote reusability.

Conclusion

The Future of Microfrontend Architecture in Angular

As Angular continues to evolve, it is expected that support for microfrontends architecture will improve, making it even easier for developers to build scalable and maintainable applications. The Angular community is likely to contribute more tools and resources to streamline the implementation of microfrontends, further promoting the adoption of this architectural approach.

Resources and Further Reading

To further explore microfrontends architecture with Angular, Module Federation and Nx Workspace, consider these resources:

Leave a comment

Your email address will not be published. Required fields are marked *