⏰ It’s time to talk about Import Map, Micro Frontend, and Nx Monorepo

⏰ It’s time to talk about Import Map, Micro Frontend, and Nx Monorepo

How leveraging native import map overrides can significantly benefit your micro frontend architecture

In this article, I am sharing about an architecture that has profoundly changed my perception of software development for large enterprises with complex infrastructure and team organization.

I’ve been eager to write this article for a long time, and now’s the time! I’ve always valued sticking to standards, and I believe the ecosystem is now ready to properly handle this architecture with the latest tools.

The advent of esbuild, the native support for ES Modules in browsers, the widespread adoption of import map, the emergence of tools like Native Federation, and the Nx ecosystem all combine to forge a flexible and well-maintained Micro Frontend Architecture.

I’ll cover:
· Real Story!
· A short reminder about browsers
· Micro frontend architecture in a nutshell
· What is an Import Map?
· Exploring the Full Potential of Import Maps and Overrides
· Nx Enables Scalable Micro Frontend Architecture
· What about Native Federation?
· Final Thoughts

Real Story!

Just to give you more context, I led the migration of several AngularJS applications to the newer Angular Framework. My client finally decided to make that move following the AngularJS deprecation announcement (stay up to date please 🙏)️.

Using the usual migration process was not possible. After investigating multiple scenarios, the micro frontend architecture was chosen. As we see, it facilitates incremental migration, provides isolation, and allows the integration of apps from multiple teams into one unified platform.

At that time, the micro frontend architecture was not yet popular and only the single-spa library was mature enough. It supports many frameworks, including AngularJS and Angular, making it a perfect choice for us!

Single-spa orchestrates the micro frontend by toggling between AngularJS or Angular implementations based on a feature flag:

Using single-spa has significantly enhanced my understanding of implementing micro frontend architecture, particularly highlighting the substantial benefits of utilizing import maps and micro frontend overrides. These tools have greatly improved my experience in local development, testing, and deployment.

I highly recommend having a look at thesingle-spadocumentation to understand the concepts ofmicro frontendand**import map.

A short reminder about browsers

To grasp the following subjects, I believe it’s crucial first to recall the basics of the web, focusing on the primary flow of a browser running a web application:

  1. The first action is always to get anindex.html file, which has everything needed to start the application.

  2. Then, the browser loads all the files that the index.html says it should. This often includes the main files for the application, like JavaScript and stylesheets.

  3. After that, the application or the user interaction leads to more requests being made, for example, calling APIs or loading parts of the site as needed.

The browser’s job is simply to load these files or assets and put them together into the web application.

Micro frontend architecture in a nutshell

Let’s start with a short definition: the micro frontend architecture involves breaking down a frontend application into smaller, more manageable pieces — each responsible for a distinct feature or domain of your application. It’s often compared to the microservices concept but at the frontend layer.

Determining the exact point at which an application adheres to micro frontend architecture can be challenging, like defining the ideal size for a microservice.

The key aspect is having a platform capable of plugging in and combining multiple pieces of functionality to produce a unified application. Whether these pieces are lazy-loaded components or micro frontend, the principle remains essentially the same.

In which situation it suits you well?

There are many use cases where the micro frontend architecture can be useful:

  • Multiple Frameworks: The most common use case involves integrating various technologies into a single product, particularly useful for unifying disparate systems.

  • Team Decentralization: When teams operate independently, within a monorepo or different repositories, micro frontends make it easier to merge their work into one cohesive product.

  • Separation of Concerns: Ideal for structuring your application into isolated domains and features for better organization.

  • Complex Infrastructure: The ability to plug a micro frontend into an existing environment can significantly enhance the development experience! We’ll delve into this reason further later on.

Don’t use micro frontend architecture if you don’t need it

Major Concepts

In a micro-frontend architecture, we distinguish various types of entities, each adhering to a distinct concept:

  • The Micro Frontend (or micro app) is loaded by the Host upon navigation or routing. Each micro frontend is responsible for a distinct feature or domain within the application. Like any app, it can contain child routes and multiple components.

  • The Parcel (also referred to as a component or expose) is loaded independently on-demand. It can be a shared component or a shared service and can be plugged in anywhere.

Tools/Frameworks

There are several implementations of the micro frontend architecture, and I’ll delve into three notable ones here:

  • Single-spa: This framework keeps things simple and works with many technologies. However, its simplicity might mean you have to do more work if you’re using just one technology.

  • Webpack Module Federation: Almost everyone uses Webpack, and its module federation feature makes micro frontends easy for these users. But, if you’re using a different tool, you might need to find another solution.

  • Native Federation: This method combines the ease of Webpack’s approach with newer tools like esbuild or Vite, fitting well with modern development practices while supporting micro frontend architecture.

What is an Import Map?

Let’s begin with the most intriguing aspect. In my opinion, the import map is an underappreciated browser technology. It is compatible with all browsers and plays a role in directly supporting JavaScript modules in the browser.

For full compatibility and extra features, we usually use the libraryes-module-shims.

How does it work?

The principle is quite straightforward. Since the introduction of the ES module into our JavaScript ecosystem, we’ve all started using syntax like:

import moment from "moment";
import { partition } from "lodash"

However, when using ES modules natively in a browser, you need to specify the full path to the JS file, something like:

import moment from "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js";
import { partition } from "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js";

This approach isn’t very readable or maintainable, is it? Therefore, the import map was created to map a library name to a URL:

<script type="importmap">
{
  "imports": {
    "moment": "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js",
    "lodash": "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"
  }
}
</script>

It functions similarly to TypeScript’s path mapping but directly in your browser. Now, you can use the same syntax whether loading modules locally or in the browser.

This import map can be specified inline or as an external file, like:

<script type="importmap" src="assets/shared.importmap.json"></script>
<script type="importmap" src="assets/remotes.importmap.json"></script>

For more information, I recommend checking out theMDN Web Docsand theproposal’s GitHub repository.

As I mentioned, the micro frontend architecture is a just a way for dynamically loading bundles from the browser and integrating them into the actual apps.

This orchestration is the role of the Host. However, when the host needs to load an ES module, it can simply utilize the JS import system and, with the aid of the import map, map the module to its location

Similarly, for Parcels, when you need to load a component on demand, the import map will map your JS import to the current location.

Import Maps are overridable!

You can declare multiple import maps in the same HTML. This means that if two import maps declare the same key, the last one will override the previous one.

By injecting a new import map into your HTML, you can hook/remap any bundle. Thus, you can replace a micro frontend, a component, or even a shared library!

I recommend the libraryimport-map-override which allows you to manage the import map directly in your browser.

Security

Overriding an import map in a web application does not inherently reduce its security, as all frontend assets are public and can be modified client-side. However, for applications that load assets from multiple servers, configuring a Content-Security-Policy (CSP) is crucial.

CSP helps whitelist trusted domains, significantly reducing the risk of Cross-Site Scripting (XSS) and other security threats. This security measure ensures that even if client-side modifications are possible, the application’s integrity and user safety are maintained.

Exploring the Full Potential of Import Maps and Overrides

Now that we understand the principles of the import map and the fact that we can override the bundles loading directly in the browser, let’s see how we can get the advantage of that concept within our development process:

Local Development

Setting up a complex local environment in a large organization often involves:

  • Spending more than a day to set up your local machine.

  • Installing a wide range of software, like backend systems, local databases or connections to external environments, local queuing systems, etc.

  • Adjusting settings for multi-tenants.

  • Take coffee breaks while you wait for your local environment to bootstrap in the morning, hoping it stays stable throughout the day.

This complexity can be quite frustrating, especially when you only need to make a minor UI adjustment. This is the exact challenge I aimed to tackle through the adoption of micro frontend architecture in combination with the import map overrides.

Instead of running an entire complex ecosystem, you can just plug your local environment into an external environment where all of the complexity is already in place.

To do so, you just need to serve your micro frontend locally and use the import map override principle on the distant environment:

After the reload, the micro frontend loaded by the browser will be not the one on the distant server but the one on your local machine.

One crucial aspect is that you are directly integrating your code into a real environment that contains the latest main branch. This means we can move past the infamous “It Works On My Machine!” scenario.

This approach showcases true Continuous Integration

Pull Request

When you’ve completed your implementation (and tested it 😋), you typically create a pull request to merge your code into the shared codebase.

Facilitate Reviews
You can once again leverage the advantage of import map overriding to make the review process easier, allowing reviewers to validate your changes without needing to deploy or clone the code locally:

At this stage, the CI will build your app and generate new bundles for the modified micro frontend. Additionally, you can generate an affected importmap.json with the updated bundles.

Simplify UI e2e tests
You can also use the affected import map for your UI tests (mocks). In this scenario, the affected importmap.json generated can be injected into tools like Playwright or Cypress to directly test the affected micro frontends.

Acceptance

This step signifies the moment when you need to confirm that your code is ready for production deployment. It can be automated on CI or manually (please automate 🙏).

Typically, this is run several times per day with the most recent codebase in an environment that mirrors production. In this scenario, you’ll generate an importmap.json that includes the latest versions of all bundles:

If the latest importmap.json generated proves successful, it can then become a release candidate for production.

Production

When your release is validated and ready, you can consider deploying it to production. Here, too, having an importmap.json offers significant advantages.

Deploy in a Sec
You can deploy/upload your bundles to production at any time. Until the importmap.json references them, they will not be loaded. Thus, deployment involves merely modifying and uploading the latest import map. This deployment process takes a mere second, requires no freeze, and is completely transparent to the user.

You should have a look to theimport-map-deployerlibrary which enable to update animportmap.json directly on the server

Keep Previous Bundles in Cache
It’s also important to note that the importmap.json can still reference bundles with previous versions. In fact, if some micro frontends have not been modified, there’s no need to generate a new version for them.

This means that users won’t have to reload these existing versions because they are probably already cached in their browser. On the other hand, the importmap.json should never be cached!

Canary Deployment & A/B Testing
One last, and not negligible, benefit of the importmap.json is that it can be generated dynamically. This means you can decide whether a micro frontend should load an old version or a new one.

As a result, you can easily conduct A/B testing or canary deployments based on feature flags or authenticated user criteria!

Nx Enables Scalable Micro Frontend Architecture

I won’t delve into all the benefits of Nx, a topic I’ve extensively covered in previous writings. I’ll encourage you to have a look at the Nx website for more detailed information.

My conviction in the value Nx brings to not just JavaScript/TypeScript repositories but to any codebase is unwavering. Its strengths in enhancing sharing, visibility, performance, and adherence to conventions are universally applicable.

Monorepo and Micro Frontend aren’t the opposite?

Not at all! A monorepo adds value through enhanced code maintenance, build, and integration processes. Conversely, micro frontend architecture delivers benefits at runtime.

Both strategies advocate for separation of concerns and reusability, showcasing significant advantages in incorporating micro frontends within a monorepo.

Nx still delivers value even if you don’t use a monorepo.

Affected micro frontends

A pivotal concept in Nx is the ability to execute tasks solely on the affected code. This feature significantly simplifies working on a single micro frontend at a time in a remote environment, streamlining local development.

By limiting actions like build, lint, and testing to impacted micro frontends, the efficiency of your CI/CD processes can be markedly improved. Utilizing an affected importmap.json that lists the affected micro frontends can enhance various processes, including testing PRs on existing environments, running e2e tests, and facilitating incremental deployments.

Single Version Policy

While independence and isolation are cornerstone principles of micro frontend architecture, sharing some services and components across all instances is inevitable.

The monorepo approach, coupled with a single version policy, ensures by design that micro frontends remain compatible with one another, fostering a cohesive ecosystem.

What about Native Federation?

Like I said at the beginning, I think now the ecosystem is mature enough to apply the same principles by using Angular, or other frameworks using esbuild, and Native Federation within an Nx monorepo.

I encourage you to have a look at theblog post annoucing Native Federation

Unfortunately, I was unable to implement the import map overrides in conjunction with Native Federation. However, this issue is currently under discussion on GitHub:

However, the underlying principles remain unchanged. Rather than directly utilizing the importmap.json, I have the option to override the federation.manifest.json. This requires the creation of custom code within the application to enable the overrides of the bundles.

Do you want to try it?

  1. First, clone my GitHub repository:
git clone git@github.com:jogelin/nx-nf.git && cd nx-nf

2. Begin by installing the packages:

pnpm install

3. Next, you can start one micro frontend, for example, mf-admin:

npx nx run mf-admin:serve

4. Then, access the URL https://nx-nf-a2d7c.web.app/admin where I have already deployed the application. You should see the application:

5. Now, open your favorite browser debugging tool and connect your local server to the remote application by adding this entry in the local storage:

localStorage.setItem('native-federation-override:mfAdmin', 'http://localhost:4203/remoteEntry.json') // override mfAdmin with you local server

6. Then, make modifications to the mf-admin micro frontend. For example, change the message from "Welcome to the Admin Page" to “Welcome to the LOCAL Admin Page”

7. After you make changes, reload the page, and you should see your modifications reflected on the remote server immediately!

8. To revert the changes, simply remove the entry from the local storage and refresh the page to see the original state again.

localStorage.removeItem('native-federation-override:mfAdmin');

You can override any micro frontend using this approach. However, as I mentioned, the method involving native federation is not entirely native yet because it doesn’t utilize the default behavior of import maps.

You can find all the code utilizing Native Federation, Angular, and Nx in my GitHub repository.

Final Thoughts

This exploration reveals the power of the native JavaScript ecosystem in browsers, highlighting how native support for ES modules enhances our development experience beyond faster build times.

The simplicity and effectiveness of the import map principle show us a way to solve complex issues with elegant solutions. It hints at a future where reliance on custom framework implementations diminishes in favor of native browser features, making development smoother and more intuitive.

Moreover, the use of Nx as part of this ecosystem offers a powerful toolkit that enables developers to approach complex projects with enhanced agility and precision.

The hope for more native features like these grows, promising a simpler, yet more powerful development landscape. With Nx and advancements in browser capabilities, we’re moving towards a future where building sophisticated web applications becomes more accessible and efficient.

🚀 Stay Tuned!

Credits

Joel Denning

Joel Denning is the visionary behind single-spa, boasting profound insights into the true mechanics of the web and is, in my view, a pioneer in micro frontend architecture. I highly recommend checking the single-spa website and his YouTube channel. While the videos might appear dated at first glance, rest assured, Joel is ahead of his time, and the content remains incredibly relevant today.

single-spa framework from Joel Denning

Manfred Steyer

Engineer, Architect, Speaker, Trainer, Consultant, and Author, Manfred knows what he is talking about. For everyone, not just Angular, interested in micro frontends and web architecture in general, I encourage you to have a look at his book Enterprise Angular: Micro Frontends and Moduliths with Angular and the blog of the Angular Architect team.

Micro frontend and moduliths with Angular from Manfred Steyer


Looking for some help?🤝
Connect with me on
TwitterLinkedInGithub