In a previous article, we introduced the micro frontend architecture. If you’re not familiar with this type of architecture, I strongly recommend that you start by reading this one. In this article, we’ll look at several design points related to this type of architecture, including communication between micro frontends and test strategy.

Communication between micro frontends

The aim of micro front-end architecture is to decouple the different parts of an application as much as possible. But most of the time, certain parts need to interact with each other. They need to be able to communicate.

How can this be done, when each micro-frontend is potentially realized in a different technology?

To do this, you can simply use url parameters, with all the limitations that implies. But there are other, more practical solutions, which we’ll look at below.

Attributes + Properties

In the particular case of Web Components, data with attributes can be passed directly via the HTML code.

<product-details product-id="1234567890"></product-details>

The example uses a properties javascript product identifier to display product details.

const monWebComponent = document.getElementById("mon-web-component")
 
monWebComponent.produit = {
   id: "1234567890",
   name: "Mon produit",
   price: 30.5
}

Benefits

The attributes approach is similar to the transfer of data from parent to child component in current SPA frameworks. So it’s an intuitive way for front-end developers.

In the case of the properties approach, some libraries even simplify their use by passing data – even complex data – as attributes.

Disadvantages

The data format passed to attributes remains rather limited, as it only accepts data in the form of character strings.

This data is directly visible in the code, so don’t use it for sensitive data!

Dom / Custom Events

In particular, the DOM is made up of an interface orchestrating the events that take place within the page, whether caused by the user (mouse click, key pressed, for example), or by other elements (paused video/audio, for example).

These events can be created and used to pass data via the window element.

There are two main principles:

  • window.postMessage()
  • Custom Events.
/* emitter.js */
const message = {
 type: "important",
 content: "Ce message est important",
};

// DOM Event
window.postMessage(message, "https://url-destinataire.fr");

// Custom Event
const event = new CustomEvent("message.important", {
 detail: message
});
window.dispatchEvent(event);

/* receiver.js */
window.addEventListener("message.important", handleMessage);
 
function handleMessage(event) {
   // la donnée est accessible dans event.data ou event.detail
}

It is preferable to use Custom Events instead of window.postMessage() in all cases except for iframe, which can only be used with the latter method (due to their particular encapsulation).

The window.postMessage() method can be called from and to any domain, so you need to be particularly careful when receiving it, for the sake of application security.

Benefits

They are easy to use.
They ensure data reactivity between micro frontends on the same page.

Disadvantages

Their use creates direct coupling between transmitting and receiving applications, which is precisely what we want to avoid.

Managing the various events can quickly become complex, depending on their number.

State Management

The component approach provided by frameworks such as React or Vue leads to parent-child dependencies for data passing.

To avoid this complexity, Redux (for React), Vuex (for Vue) and NGRX (for Angular) state management solutions enable all components to have the same level of access and write access to the application’s shared data, made independent of the components.

We can apply this concept to several micro frontends: one solution is to attach our read (selectors/getters) and modify (actions) methods to the window.

Benefits

Shared data is available anywhere in the application, and independent of any micro frontends.

It persists regardless of the life cycles of micro frontends.

Using Vuex, Redux or Ngrx, you can keep your data up to date reactively.

Disadvantages

However, this solution (via window) remains dangerous : anyone can access this element via the browser console and thus have read and write access to all common data.

Session Storage / Local Storage / Cookies

Data communication within a micro-frontend architecture can also involve browser data storage solutions. Here are the 3 main ones:

  • sessionStorage stored data can only be accessed within a tab. It does not survive when the tab is closed.
  • localStorage stored data is shared by all browser tabs originating from the same source. It survives browser and system restarts.
  • cookies The stored data can be accessed by all browser tabs originating from the same source. The lifetime can be adjusted as required.

Benefits

Like state management, the main advantage of these methods is that they provide read and write access to global data at any point in the application, and are totally independent of page changes.

Disadvantages

The same disadvantages apply as before: this data can be accessed and modified by the user directly from the console, so beware of application security.

Stored objects are in string format only.

This data is “rigid”: a modification will not lead to a dynamic update in applications, but only to a refresh or page change.
The volume of data on localStorage is limited per application, depending on the browser. The same applies to cookies, where the limit is even lower.

Communicating with an API

If we have separate teams working independently on each micro frontend, what about backend development?

The advantage of full-stack teams is that they own the development of their application, from visual code to API development, and so on.

The model to be applied is the BFF pattern: Backend For Frontend.

Each front-end application has a dedicated backend whose purpose is to meet the needs of that front-end only (originally this model was intended to have a backend for mobile, one for desktop, etc.).

The BFF can be autonomous, with its own business logic and database, or it can be a service aggregator.

The idea here is that the team building a micro frontend shouldn’t have to wait for other teams to build things for them (i.e. wait for a web service to be developed by the backend team and, in the meantime, use mocked-up data which, in the end, may not correspond exactly to the data issued by the service).

So, if every new feature added to a micro-interface also requires back-end modifications, it’s a solid case for a BFF, belonging to the same team.

On the other hand, if the micro frontend has only one API with which it exchanges, and this API is fairly stable, there may not be much point in creating a BFF.

Illustration of the Backend For Frontend pattern within a micro frontend architecture
The Backend For Frontend pattern

How should a user of a micro frontends application be authenticated and authorized with the server?

Users should only have to authenticate once. As a result, authentication must belong to the container application.

The container app probably has some kind of login form, through which we obtain an access token. This token should then belong to the container and could be injected into each micro frontend when it is initialized.

Finally, the micro frontend can send the token with any request it sends to the server, and the server can perform the required validations.

Test strategy

The micro-frontend architecture model creates technical complexity.
Technical complexity requires a solid harness of automated tests, without which our superb architecture risks being a fiasco.

As a reminder, automated testing is mainly broken down into unit, integration and end-to-end tests (see OCTO article: The testing pyramid in practice).

  • Unit tests verify the behavior of a portion of code, totally or partially isolated from its dependencies.
  • Integration tests verify that several components work together.

As each micro frontend is an independent application, it must have its own unit and integration tests.

In addition to these tests, we can add contract tests, halfway between unitary and integration tests, independent of the micro frontends, which ensure that inter-application communication is operational by checking that interfaces are respected when data is sent and received.
So, if there is a breaking change, we know which micro frontend is responsible. Finally, end-to-end testing is used to check that a user journey is running smoothly from start to finish.

It’s these tests that will ensure that the micro frontends work together coherently and that the application is usable.

These tests take longer to run: the less interaction there is between the micro frontends, the more succinct the tests will be, and therefore faster to execute.

However, a change to a micro frontend leads to a change in the overall application: our application’s CI must run end-to-end tests every time, and once again it’s in our interest to make the micro frontends as independent as possible.

N.B.: the Cypress end-to-end testing framework has the advantage of being able to bypass Shadow encapsulation.