Skip to main content

Introduction to micro frontend architecture

Micro Frontend is a design pattern which enables splitting large and complex web applications into lightweight web applications. Each of these micro frontends can be managed by a different team with the freedom to choose their own framework (Vue.js, React, Angular, etc.) in the implementation. This allows every micro frontend to have its own git repository, its own package.json file and build tool configuration. The result is having an independent build process and independent deploy / CI, which in general provides fast build times for each repo. All of these loosely coupled web apps are then integrated into a single container or user interface to create a seamless experience. 

Advantages of using micro frontend architecture

Great number of single page applications or isomorphic web apps have been developed and deployed over the years. The size and complexity of these web/apps grew and posed similar challenges to that of monolithic applications. Now, these applications face multiple challenges and issues like performance degradation with each release, non-scalability, and difficulty in making modifications and ensuring maintenance. Micro frontend architecture is the answer to all these issues.

  • Modular, reusable, cohesive and manageable code base
  • Possibility to create technology agnostic solutions that can be developed by independent teams.
  • Web App will be extremely contextual, intuitive and responsive
  • Allows the leveraging of lazy loading to improve the overall frontend performance.
  • Gives the ability to upgrade the sections of the frontend independently
  • Multiple micro frontends could be loaded on the same page without refreshing the page
  • Each micro app could be developed using a different frontend JavaScript framework like Angular, React, Vue.js etc.
  • Offers an easy way to share a common access token between child apps
  • Better control in terms of authentication and authorization
  • Separation of concerns as each micro frontend’s design is based on a specific use case or business need
  • Small bundle size of each micro frontend delivers better performance
  • An independent CI/CD pipeline can be built for each micro frontend and can be deployed without affecting the others
  • It offers a highly scalable architecture
  • Small and structured code base which gives more control on refactor, change and maintenance of the code
  • Speeds up the development process
  • One micro frontend can pass down initialization information, like the rendering target, to another
  • Common event bus reference can be passed between micro frontends to enable them to talk to each other
  • It supports mono-repo architecture as well, meaning all micro frontends can built on top of common reusable libraries/components

Challenges encountered using this approach

  • Debugging across multiple micro frontends can sometimes be challenging and tedious
  • Because every team is working independently, there are high chances of deviation in folder structure and coding standards, there is a possibility of redundant code, duplication of dependencies or references
  • Micro frontends developed using different JavaScript frameworks increase maintainability requirements
  • Not recommended for SEO requirement

Single Spa – Short overview

There are a lot of frameworks out there that can be used to implement this architecture. One of the most popular of these frameworks is Single SPA.
Single SPA is an open source project, for bringing together multiple JavaScript micro frontend applications, by simplifying the composition of multiple front-end applications into a single product.

So it’s basically an NPM package that controls the mounting and unmounting of your micro-frontends. You can think of it as a top level router which when a route is active it will download and execute the code for that particular route.

Micro Frontend Reference Architecture

Elements of Single SPA

Single SPA config (aka root config)

  •  Root HTML file
  •  JavaScript that registers applications
  • Applications

Looking at the different elements that Single SPA consists of, there are two main elements. First is the Single SPA config which is usually referred to as a root config. That is the root HTML file that is shared by all the Single SPA applications.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>N47-Cockpit</title>
  <meta name="importmap-type" content="systemjs-importmap" />
  <script type="systemjs-importmap">
    {
      "imports": {
        "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
        "ht-equipment-desk-frontend": "<%= equipmentDeskUrl %>",
        "ht-skills-portal-frontend": "<%= skillsPortalUrl %>",
        "frontend": "<%= helloTodayUrl %>"
      }
    }
  </script>
  <script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.js"></script>
  <link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' rel="stylesheet">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
  <link href='https://fonts.googleapis.com/css?family=Poppins&display=swap' rel="stylesheet">
</head>
<body>
  <noscript>
    You need to enable JavaScript to run this app.
  </noscript>
    <div id="app"></div>
  <import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>

and some JavaScript that registers the applications

import { registerApplication, start } from "single-spa";
import { Apps, RegisteredChildApps, ExposedChildAppFunctions } from "./types/config";

const System = (window as any).System;

export const registeredChildApps: RegisteredChildApps = {};

const registerExposedChildAppFunctions = (
  appName: string,
  registry: RegisteredChildApps,
  exposedFunctions: ExposedChildAppFunctions
): void => {
  registry[appName] = exposedFunctions;
};

const apps: Apps[] = [
  {
    name: "ht-skills-portal-frontend",
    pathName: "skills-portal"
  },
  {
    name: "ht-equipment-desk-frontend",
    pathName: "equipment-desk"
  },
  {
    name: "frontend",
    pathName: "hello-today"
  }
];

window.addEventListener("register-apps", (e: Event) => {
  apps.forEach(app => {
    registerApplication({
      name: app.name,
      app: async () => {
        const module = await System.import(app.name);
        if (module.exposedUtilityFunctions) {
          registerExposedChildAppFunctions(app.name, registeredChildApps, module.exposedUtilityFunctions);
          if (module?.exposedUtilityFunctions?.setToken) {
            module.exposedUtilityFunctions.setToken((e as CustomEvent).detail.keycloak.token);
          }
        }
        return module;
      },
      activeWhen: location => location.pathname.includes(app.pathName)
    });
  });
});
start({
  urlRerouteOnly: false
});

To go in more detail, you need to register the applications with single-spa, so it knows how and when to initiate, load, mount and unmount them. When you register an application, you need to include three things: give it a name, supply a function that will actually load the applications code, and you need to supply a function that determines when the application is active or inactive. Additionally there should be a start function (that calls the start API) in order for the applications to be mounted. Before calling the start function the applications will be loaded, but they won’t be bootstrapped, mounted or unmounted.


The second main element is the applications, which you can have an N number of. Each application is responsible for implementing the following API:

  • bootstrap()
  • mount()
  • unmount()

The bootstrap function initializes the application, and it’s called only once, right before the registered application is loaded for the first time. And the self explanatory mount/unmount functions. All these functions need to return a promise or be an async function. Once that is defined, single-spa will do the rest.

Summary

There are plenty of ways to approach micro frontends these days. The given entry is only a small fragment introducing you to design an application with the use of a micro frontends. My recommendation is to check a variety of patterns and solutions before you start implementing this approach. There isn’t any standard as of today for implementing micro frontends. By choosing this approach you’ll definitely have a lot of advantages when compared to the monolithic approach. Just remember that the monolithic architecture is not necessarily a bad choice. It all depends on the specifications of the project and the team working on it, you should always strive to facilitate and simplify solutions as much as possible.

I hope this article gave you a brief idea about micro frontends, and if you are interested in exploring more about Single SPA, you can refer to their documentation.

Thanks for reading!