Building user interfaces in the dynamically changing world of cloud applications can be challenging. Teams are spread across the globe. Developers have ambitions to constantly learn new technologies. React, Angular, or Vue? Which one to choose? The decision to adopt a certain framework might turn out to be irreversible. We tie up with our choice for years, maybe sometimes looking with a sigh at the newer, better, and cooler technologies. A question also arises as to how to organize work on the multi module user interface. Is there a need to create a dedicated team for maintaining the whole project? Or will it be better if each team contributes with their own interfaces? But who will be the main owner then? What if there is no possibility to create one team focused only on interfaces and we have to split the work between multiple full-stack teams working in their own rhythm? And what if suddenly, due to “The Great Unification Strategy”, we will have to join two products, created in different frameworks? How to quickly integrate such two separated projects and deliver the best user experience possible?

There’s a lot of problems when it comes to creating a multi-module UI application. Some of them can be solved by building a client side part of the system basing on the micro-frontends architecture. Of course, this method won’t magically solve all issues. Choosing such an architecture requires a deep analysis of all pros and cons. However, in the project where I had the opportunity to work, we’ve chosen this architecture. As a tool to help us with it, we chose the Luigi project — an open source micro-frontend orchestrator delivered by SAP. In this article, I will try to explain how to deliver a valuable project using Luigi.

Micro-frontends

What are micro-frontends?

At the beginning, let’s briefly describe what micro-frontends are. If you’ve never heard of them before, the article at https://micro-frontends.org/ will help for sure.

In a nutshell, this architecture relies on building one complex UI application based on multiple independent micro applications. Of course independent on the code level, boilerplate (dependencies management, way of building, etc.) or deployment. Eventually, these micro-frontends should make one product with consistent user experience. In this approach, each micro-frontend can be developed by any team in any technology. Huge independency ensures that a given functionality can be delivered end-to-end (backend, frontend, deployment) by the team which is best prepared to do it. The downsides of this approach, none of which are impossible to overcome, can make it more difficult to provide a consistent user experience. Also, there can be additional, duplicated work related to all aspects of the project, like continuous integration, code scans, dependency vulnerabilities monitoring, etc. However, in the projects where the goal is to release regularly, micro-frontends can be the right choice. This approach make it easier to ensure parallel team work and reduce dependencies in the development process. This architecture can also simplify potential customization of the product for or by the customer. That’s because out-of-the-box this concept implies the possibility of easy extending by interfaces that doesn’t even need to be based on the same technology stack.

However, micro-interfaces will work not only in a huge enterprise solution, but also in much smaller hobby projects, where they allow you to play around with different technologies by joining them together into one consistent application.

Real life example

Enough of theory. So how does it work in real life? In the case of the project that my experience is based on, the concept of micro-frontends was used to build a backoffice type application for the system focused on collecting e-commerce customer data. This data is collected to prepare the best possible storefront personalization for the end customer, for example by displaying a product carousel matching the customer’s interests. It’s quite a large and constantly rising cloud, micro-services-based application. The system is build by couple of teams spread across the globe. Each team is responsible for one piece of the business functionality and delivers it end-to-end, building both backend and frontend. In total, there are more than 20 user interfaces — from the administrative ones, through the ones related to personalization and other business features, to the analytics with charts and reports. Spreading the work between teams who have the best knowledge about a specific domain, certainly speeds up and simplifies the releasing process. The fact that each micro-frontend has its owner and is developed independently solves the problems even in trivial situations, for example when one team breaks the build for the other, or lets the UI to be deployed to the test environment in any form and at any time.

At some point we’ve also faced the necessity of joining two independently developed products into one. The use of micro-frontends made this process much less painful than having join two UI monoliths. We were able to migrate UIs iteratively and it freed us from the necessity of rewriting the existing apps to use the same technology.

The Luigi project was of invaluable help in this process. It made it easy for us to connect everything into one piece and let us focus on further delivering the business value.

Luigi

When building an app based on the micro-frontends you will need a coordinating component. In your project, you will undoubtedly need navigation, perhaps also some sort of login mechanism or permission handling. For sure it will be useful to also have some common functionalities like alerts, state management, routing etc. Here comes Luigi which is such coordinator. Luigi will fit into the projects regardless of their scale. With it, it will be possible to:

Build a small hobby project to play around with different technologies. With Luigi, joining user interfaces written in Angular and React into one consistent app is harmless.

Build a multi-module enterprise application and extend it with new interfaces for years. If needed, interfaces can be gradually updated to newer technologies.

Quickly deliver a functional system/solution proof-of-concept.

How does it work?

Tl;dr

Add Luigi to the project dependencies. Create a simple configuration object in which micro-frontends URLs are specified. Optionally, register a client library in micro-frontends to interact with the orchestrator. Use.

Example application

I like riding a bike. In the biking season, whenever possible, I try to spend one day of each weekend in the bike park with my friends. However, these trips create some problems which could be solved by a simple application.

These problems are:

choosing a bike park with good weather

calculating the total amount of money for fuel, including also car exploitation costs (for one trip it doesn’t matter but for multiple trips in the year it becomes a larger amount)

Application solving such problems could contain:

weather module, showing the weekend weather forecast in selected bike parks,

calculator module,

checklist with all items to bring and things to do before the trip

To let everyone use such an application, an authentication and persistence layer would be useful. Of course, I could build the whole UI using i.e Angular, but:

I’d have to do a lot of things from the beginning, i.e oauth2 support,

I’d have to build a menu,

I already have a weather module,

I’d be limited to one technology, but I’d like to try i.e React or Vue and in the future maybe something different.

In this case, using Luigi as an orchestrator for my micro-frontends solves my problems. That’s why I used it to build an example application.

Screenshot of the released application

Usage

Orchestrator

Below is the example configuration of Luigi project. I won’t be describing all features here, because there’s a lot of them. I will describe only the features that I decided to choose.

settings section describes the application’s looks and behaviour by menu settings, logo, title, and so on.

section describes the application’s looks and behaviour by menu settings, logo, title, and so on. routing section configures the behaviour of the navigation. It’s possible to specify whether the application should use a hash-based navigation, or a path-based one. It’s possible to also define here the prefix of the query parameters that will be read by the orchestrator and passed to the micro-frontends. Micro-fronteds placed in iframes have no access to the parent’s window.location object, so luigi-core must know which parameters are dedicated for the presented views.

section configures the behaviour of the navigation. It’s possible to specify whether the application should use a hash-based navigation, or a path-based one. It’s possible to also define here the prefix of the query parameters that will be read by the orchestrator and passed to the micro-frontends. Micro-fronteds placed in iframes have no access to the parent’s object, so luigi-core must know which parameters are dedicated for the presented views. navigation section defines the structure of the app’s navigation. In the above config example, my simple application contains two registered micro-frontends. First of them, weather , is available without the authentication, and the second one, todo , will be displayed in the menu only for the signed-in users. Both interfaces are under the same trip path, called context root, and it will be possible to access them under e.g http://example.com/#/trip/weather.

To simplify the config object, I skipped the authentication section. This one can look as shown below:

he above configuration refers to the Oauth2 Implicit Grant Flow mechanism, and uses the provider from the @luigi-project/plugin-auth-oauth2 package. Alternatively, it’s possible to use OpenId Connect support delivered in @luigi-project/plugin-auth-oidc , or create an own provider.

In the above example, all fields added to oAuthData will be sent as the requested parameters. The nonce parameter is automatically generated by Luigi, but it’s possible to overwrite it with your own logic.

The path to callback.html page is given as the redirect URL. Callback page contains the code which can read and store data retrieved from the authorization server response. This code can be copied from the identity provider’s package. I copied it to the dist folder using copy-webpack-plugin — new CopyPlugin([‘node_modules/@luigi-project/plugin-auth-oauth2/callback.html’]) .

To run the orchestrator, the main page HTML code must contain:

<div id=”root"></div> element,

element, script with the above configuration.

That’s it!

Micro-frontends

To take full advantage of Luigi’s potential, micro-frontends must be able to communicate with the orchestrator. The communication between iframes is possible thanks to the native javascript postMessage mechanism. The Luigi-client library which is dedicated to be used in micro-fronteds, covers this low-level mechanism and shares API to interact with the core and register listeners for different application live cycle events. Thanks to that, there is a possibility to use functions that facilitate the navigation, or or display of notifications, alerts, and the like.

Some of the micro-frontends that I prepared use Firebase Realitme Database. To authorize the database client, it’s necessary to use oauth idToken returned in the authentication process. This, in turn, is stored in the parent’s window local storage, so it’s not possible to just read it from the child frame. Instead, it’s possible to pass it as a property of the object which will be passed as the context to the selected app. The context is defined in the routing configuration and it makes it possible to pass any data that should be available during its initialization.

Initialization handler, which allows you to access the context data, can be registered using the LuigiClient.addInitListener function:

If the application requires context data to start, the listener will be a good place in which to run the application manual bootstrap process, like for example React's ReactDOM.render .

the listener will be a good place in which to run th

Script which I prepared is tiny, so there weren’t many chances to use the full set of Luigi’s features. Therefore, my usage of luigi-client features was limited to the registration of initialization handlers or for example displaying alert popup using the LuigiClient.uxManager().showAlert() method:

Alert example

One of the useful features that Luigi provides, is the possibility to link to another interface (data can be passed through query parameters in this way) and store the state of the navigation initiator interface. For example, when the user is filling in the form, and it requires some data available in another interface to be entered, the user can open the other interface, check the required data, and return to the original form, which will be in the same state as it was before they left. Under the hood, a new iframe just covers the old one, but it’s invisible for the end user.

All luigi-client features are of course described in the documentation.

Work results

The code of the example orchestrator application that I created is available here: https://github.com/aartek/luigi-trip.

In this project, I use Luigi v. 1.0.0.

The working application is available at https://trip-fc58b.firebaseapp.com/#/trip/weather.

Micro-frontends:

Weather (Angular 8): https://github.com/aartek/openweathermap-weekend-forecast/tree/firebase.

To-Do list (Vue.js): https://github.com/aartek/luigi-trip-todo.

Car exploitation costs: (React.js) https://github.com/aartek/luigi-trip-my-car.

Costs calculator (Angular 8): https://github.com/aartek/luigi-trip-calculator.

Microfrontends use Firebase Realtime Database to store data. They’re styled with SAP fundamental-styles.

Summary

In this post, I’ve presented Luigi basic usage possibilities. I hope that I’ve been able to clearly present the point of this solution, potential use cases, as well as show how easy it is to start working with it.

Luigi will work both in small home applications and large systems, that needs to be prepared for constant changes, migrations and stakeholders ideas. Easy extensibility of new interfaces is a huge advantage worth considering during potential evaluation of this solution. Luigi can become an excellent choice whenever “time to value” matters — you configure, you build the business interfaces, you release.

The biggest advantage of Luigi is probably that it was created and it is developed based on real requirements of real teams building real products in quite a large company that SAP is.

==

PS Luigi’s Youtube channel has been recently started 🎬https://www.youtube.com/channel/UC5WsYsHapDlg2K3iXS4n4AQ