Let’s build a Webshop out of Micro Frontends

von Marlon Alagoda | 2. März 2018 | Allgemein, English, Software Engineering

Marlon Alagoda

Senior Developer (ehem.)

The idea behind Micro Frontends is to think about a website or web app as a composition of features which are owned by independent teams. Each team has a distinct area of business or mission it cares about and specialises in. A team is cross functional and develops its features end-to-end, from database to user interface. – micro-frontends.org

In an ideal world a single team has a mission or business value to fulfill and has all tools to be capable to do so in its hand. With traditional web applications this is impossible to realize. No matter which technology you choose, in the end your React.js, Angular or Vue.js application gets compiled to a huge frontend monolith. However, similar to microservices in the backend you should enable your team to have their own release cycles, tools sets and deployment. If a team’s mission is to build a webshop’s cart application it should be able of fulfilling this task by developing everything from storage and logic to the UI and therefore be a real vertical team. To allow vertical teams in combination with web applications you need a UI composition strategy to sew everything together at one or the other point.

I’m going to investigate a way where Micro Frontends are composed at runtime, allowing each team to be really independent and guaranteeing the highest possible evolvability for the overall web application. On the backend, services can call each other synchronously via a well defined REST interface or asynchronously listing to a event store if you like even more de-coupling. I’ll desperately try to avoid communication of the Micro Frontend’s UI to keep dependencies even lower. That means the cart UI does not communicate with the product highlight UI, only their business logic in the backend does. The goal is to find a way to build Micro Frontends which are fully functioning on their own and seemingless can be combined to a bigger web application.

User Micro Frontend, fully functioning on it’s own:

The same User Micro Frontend, included in a webshop with loaded CSS:

As we know building microservices implies a huge overhead while developing, since there can be a lot of redundancy at nearly every aspect of development and orchestrating can be a pain the the ass. This fact does not change for frontend applications. On top of that Micro Frontends introduce a whole new set of challenges including CSS and JS scoping, consistent URL paths, layout management and so on.

Are the advantages big enough to accept the overhead?

Not only because ThoughtWorks thinks it’s a good idea to have a look at it, also because I like to try techniques which allow vertical teams and reduce complexity I used the x-mas holidays to look through a couple of frameworks and blog articles and implemented a proof of concept of what I think is the easiest way to build Micro Frontends.

Now let’s solve all the challenges

It took me quiet a time to understand all challenges that needed to be solved in order to combine multiple Angular, Vue.js, React.js or whatever apps into one single application.

Real Micro Frontends, not multiple SPAs

First I’d like to distinguish real Micro Frontends from multiple Single Page Applications. If you browse through Amazon, you’ll realize each area looks a little bit different. From the design and features you can feel that the whole checkout process is obviously developed by a completely different team than the product area. They are completely different web applications.

It is reasonable to have teams, each building different web applications – like a checkout process which then get linked to a bigger web application such as Amazon, if each web application is runnable by it’s self.

However, I want more!

I am aiming to have multiple Micro Frontends rendered on a single view!

Tailor.js – which is part of Zalando’s Project Mosaic – is a layout service and plays the biggest part in my Micro Frontend solution. In Tailor.js you write layouts, that define which Micro Frontend should be called on each view. Micro Frontends are called Fragments. Tailor.js calls all Micro Frontends defined for a certain layout and streams them as a single read stream to the frontend. Layouts are defined in HTML having some custom elements and attributes. A Tailor.js layout could look like this:

<!doctype html>
<html lang="en">
    <head></head>
    <h1>A website</h1>
    <body>
        <fragment slot="content" src="http://app1.micro-frontends.com"></fragment>
        <fragment slot="content" src="http://app2.micro-frontends.com"></fragment>
    </body>
</html>

Custom attributes like primary attached to the <fragment> element can be used to define the primary Micro Frontend, which then sets the HTTP status code of the overall request or public for Micro Frontends to which headers are not passed to since they could contain cookies and other private information.

The streaming application is implemented in Node.js. It takes only a few lines to spin it up an express.js app, register Tailor.js as middleware and point it to your template files.

const config = require('config')
const express = require('express')
const Tailor = require('node-tailor')
const tailor = new Tailor()
const app = express()
app.use(express.static('public'))
app.use(tailor.requestHandler)
const port = config.get('layoutServer.port')
app.listen(port, () => console.log(`Tailor server listening on port ${port}`))

Additionally you can define Tailor.js middlewares to further manipulate the request, e.g. add or remove certain headers or apply certain base-templates to prevent redundant definition of HTML boilerplate code for each template.

Tailor.js also takes part of composing the JS and CSS files each Micro Frontend has. You simply provide Tailor.js the Micro Frontend’s HTML as request body and JS and CSS as header fields.

res.set({
    Link: `<http://domain.tld/style.css>; rel="stylesheet", <http://domain.tld/bundle.js>; rel="fragment-script"`,
    'Content-Type': 'text/html'
})
res.end(html)

URLs, routing and layouts

For my case I defined three layouts.

  1. An index which contains the product-highlight Micro Frontend as a primary fragment
  2. The cart layout for the cart overview as well as the checkout process
  3. The user layout for login and user data overview.

Each layout contains a cart widget in the top right corner as another Micro Frontend.

To keep track of all possible paths to your layouts I use Skipper, which is a HTTP proxy and is also part of Zalando’s Project Mosaic.

# Calling index template
ui_index: Path("/") -> setPath("/index") -> "domain-pointing-to-tailor.tld";
...
# Calling cart template
ui_cart: Path("/cart") -> "domain-pointing-to-tailor.tld";

On top of that having a single entry point for all backend-calls simplifies your CORS setup, certificate management and minifies exposed routes and their authorization setup. I’m prefixing my service paths with their Micro Frontend’s name, in order to separate them and prevent confusion.

# Rewrite POST domain.tld/user/login to user-service.tld/login
api_user: Path("/user/*") -> modPath("^/user", "") -> "domain-pointing-to-user-mf.tld";

You can even choose a request destination by HTTP header or query parameter. You might want to show a different layout if the client does not provide an authentication header.

You want a Progressive Web App?

Now we got most of the parts together! A layout and streaming application calling and composing our Micro Frontends and a HTTP proxy for managing URLs and paths. Next I asked myself the question how multiple React.js or whatever applications interact and behave once rendered on the same page. They don’t. I use server side rendering for all Micro Frontends. I thought about composing multiple client rendered Progressive Web Apps together, however, it’s much easier and cacheable to render your applications on the server and simply send a single composed HTML to the client.

So what really is a Progressive Web App? Google states it’s a reliable, fast and engaging web application. Well, our server side rendered application can definitely meet all of these requirements. Maybe it’s even easier to build reliable and fast applications with server side rendering.

Also, I made a bold statement at the beginning of the article:

I’ll desperately try to avoid communication of the Micro Frontend’s UI to keep dependencies even lower.

Amazon, REWE, Google and Zalando, all have full page reloads when you switch to a different URL path. So I don’t think page reloads – which allow your backend services to communicate – are bad at all. But I do think that there might be cases where you want your UIs to communicate. I think those cases are far less then many of us initially think, but updating a cart widget should not require the page to reload. I don’t see any problem in introducing a client-side pure Javascript message broker each Micro Frontend can write and read from, if the pain of reloading a web app becomes too big.

Mastering dynamically created CSS per Micro Frontend

Unfortunately CSS cannot be scoped by it’s position in the DOM tree. However there are a couple of frameworks handling CSS scoping for you, depending on the web app technology you are using. I use Styled JSX which let’s you define CSS inside your React.js module and adds a hash-prefix to each class, making it unique.

<div>
    <p><a href="/user">Anmelden</a> ¯\_(ツ)_/¯</p>
    <style jsx>{`
        p a {
          text-transform: uppercase;
        }
    `}</style>
</div>

During rendering I let the framework create the CSS, which can differ from request to request, since it depends on the actual HTML being rendered. Imagine a cart widget having between zero and five articles. The cart showing 5 articles might render different components and since the CSS is defined per component it has a different CSS. However, since there is an ending number of possibilities how your Micro Frontend can be rendered, eventually each possible CSS will be created. Once I rendered the HTML on the server and created the CSS, I check if it already exists by comparing the new hash the Styled JSX module produces to the CSS hashes I already produced during earlier requests. If it’s a CSS which has never been created before I store it to an S3 bucket. Additionally, I configured S3 to be a blazingly fast static file server for my CSS files.

Let’s work together smoothly!

There are a couple of more things to think about. For example you need to make sure your CORS setup allows your web application to call your Micro Frontends and configure your XHR requests on the client-side to pass credentials like cookies, authorization headers or TLS client certificates through cross-site requests to your services. If you want to build a Micro Frontend application by yourself, have a look on my implementation for those details.

Solution, solution, solution