Microfrontend Best Practices

Repository Structure

A microfrontend is a self-contained full-stack application, and while it consists of different components, it’s a good idea to keep them in one repo. The reason for this is that this is an atomic application anyway, meaning that you can’t use the parts separately in other contexts anyway, and they share a data model. The folder structure would typically look like this:

frontend/
backend/
infra/
.gitlab-ci.yml
README.md

In such a setup, you have at least a client-side “frontend” part and a server-side “backend” part, each containing individual “projects”, i.e. they have their own dependencies and structure. For example, in the frontend folder, you will find a package.json file as well as common JavaScript tooling. Depending on the server-side technology, you will find a similar setup in the backend part as well.

Also, there should be a CI/CD pipeline configuration (Gitlab CI in this case) to deploy the microfrontend from the repository. The pipeline has the usual steps to test and build the software, but also to deploy it to a cloud infrastructure.

The configuration for this infrastructure is maintained in the infra folder (“Infrastructure as Code”). If it is just one file, e.g. a Cloudformation template, it can also live on the top level of the repository.

Frontend

Frameworks

A Microfrontend is a special type of frontend artifact. It is relatively small (compared with a Single Page application), it doesn’t share state with other microfrontends, it doesn’t need to take care of routing.

In most cases, no framework is needed at all. Look, for instance, at our example microfrontend which is simply built with Vanilla JS and a few lightweight helper libraries.

If you must use a framework by all means, please pick a suitable one, such as LitElement or Stencil. Make sure to configure the build chain in a way that it produces an ideal artefact, especially disable all transpilation to old JavaScript versions.

Browser support

We generally support browsers with a market share larger than one percent. Regardless of market share, we do not support Internet Explorer or Edge versions lower than 79.

Polyfilling and Transpilation

No code should be transpiled to ES levels lower than 2018. Also, polyfilling functions for outdated browsers is not necessary. Developers are encouraged to use modern language features and APIs to give customers the best possible experience.

Loading Fonts

Fonts cannot be loaded from within a microfrontend using the @font-face CSS rule. What does work, however, is loading fonts via the FontFace JS API. For example:

const _loadFont = (file, style, weight) => {
    const font = new FontFace(
        `mfe-demo_tuitype`,
        `url(https://static.tui.com/assets/v2/fonts/tuitypelight-${file}.woff2)`,
        { style, weight }
    )

    font.load().then(fontface => document.fonts.add(fontface))
}

const _fonts = [
    [ "regular", "normal", 400 ],
    [ "bold", "normal", 700 ]
].forEach(font => _loadFont(...font))

Then, in the microfrontend’s <style> section, you can use something like:

* { font-family: "mfe-demo_tuitype", Arial, sans-serif; }

As you see, the font name is prefixed in order to not collide with potentially already registered fonts.

IMPORTANT: Only load fonts that you need! Load fonts from the TUI Static CDN to benefit from caching!

Responsiveness

The OpenMFE spec demands that the MFE adapts its width and height to the size of the containing element. A simple implementation could rely on fluid width alone. However, in a real-world scenario it may be necessary to adjust font-sizes, image sizes and visible/hidden content. This is currently not possible with pure CSS, therefore it is recommended to use the resize observer API or an off-the-shelf implementation of container queries.

Browser and CDN Caching

  • When setting up your deployment, make CDN invalidation a part of it.
  • Do not set the expiration TTL of the main JavaScript file higher than 10 minutes.
  • Use the Cache-Control header with a max-age directive to control client-side caching, avoid Expires, as it is obsolete and limited.
  • Do not set the max-age of secondary files (i.e. files that are loaded from your main file) higher than 10 minutes, unless you have implemented cache-busting.
  • Do If you do not use cache-busting on secondary files, they also must not have a max-age higher than 10 minutes.
  • To properly implement cache-busting, insert a version number into the file name or path. Do not use query parameters, as they are not guaranteed to work with CDNs. A new version number must be generated on every build.

To learn more about HTTP caching, visit MDN’s excellent tutorial.

Tracking/Analytics

A microfrontend must not embed tracking solutions such as Tealium directly. Instead, it may emit an event that can be consumed by the host environment and from there be propagated to a third-party solution such as Tealium. If such an event is triggered, it must comply with the following provisions:

  • The event must be an instance of CustomEvent.
  • The event name must be openmfe.analytics.
  • The payload has to be stored in the detail property of the event object.
  • The payload data must be an object with the properties source, name and data.
    • The meta object must a source and property with the name of the MFE component.
    • The name property must be a string and contain the actual name of the event. It must only contain lowercase letters, numbers, dots, underscores and dashes.
    • The data property must be an object and may contain arbitrary data.

An example event could look like this:

this.dispatchEvent(new CustomEvent(
    "openmfe.analytics", {
        detail: {
            source: "tui-demo-component",
            name: "order-button.clicked",
            data : {
                hello: "world"
            }
        },
        bubbles: true,
        composed: true
    }
))

Interfaces

APIs are interfaces, and as such they should follow common best practices: They should be well-defined, hide implementation details and be stable over time, just to name a few. However, the question is, what is the interface of a microfrontend? In our case, it’s the configuration through attributes, events and functions of the web component. These need to be specified in a manifest file, as postulated by the OpenMFE specification.

The manifest is hugely important: Not only does it allow to automatically generate documentation for each microfrontend, but it also enables automated quality checks. With the manifest, an instance of the microfrontend can be generated without specific knowledge of the implementation, and the instance can be tested to be compliant with architectural principles. By storing the history of the manifest in consuming repositories, we can ensure that there are no breaking contract changes (or, at least we get alerted in such a case). Last but not least, it allows a more reliable integration into a consuming context.

Server-side

The server-side part of a microfrontend acts as a middle layer or “backend for frontend” (BFF). This is a special type of backend service which does not necessarily expose a clean and coherent data model, but instead is designed to deliver preprocessed and arbitrarily structured data to a frontend in order to reduce the network load and client-side processing.

It is usually implemented as a Docker container (with some sort of orchestration/autoscaling mechanism such as EKS or ECS) or a serverless function such as AWS Lambda. We strongly recommend serverless solutions here, unless there are good reasons to go with containers.

General Considerations

The BFF is a stateless service, which belongs only to that particular microfrontend stack and must not be shared with other consumers. It serves the following purposes:

  • Authorisation: Services often require some sort of authorization. In order to avoid putting credentials into the frontend, you will keep them on the server-side; allowing you to control who is accessing APIs in your name.

  • Aggregation: Sometimes the business logic of a microfrontend needs to collect data from multiple APIs and merge it into a custom data package for its frontend.

  • Transformation: API results can be very “heavy” and also reveal information which should not be exposed in the context of the given business case. In this case, the middle layer will transform the API response into

  • Caching: The middle layer can act as a kind of an “edge” cache to keep frequently needed and rarely changing data near to the frontend. Note that in many cases it makes sense to extract the caching logic from the middle layer into a dedicated caching service within your microfrontend stack.

Note that the API between the client-side and the server-side part of the microfrontend is not a public API. Therefore it does not need to follow any standards such as REST. In fact, it is recommended to shape the data that is passed to the client in a way that it can be consumed right away. Having all transformations and aggregations happen on the server side will make the code that runs in the browser much more lightweight. Also, as the frontend and the backend are intentionally tightly coupled, the API between the two does not have to be versioned and is allowed to change without backwards compatibility at any time.

A BFF is a private service of exactly one microfrontend; it must never be shared with other consumers.

CI/CD

Injecting Configuration

All build artefacts of an MFE must be environment-agnostic. This means that during the build step, no configuration should be injected into any artefacts (neither frontend nor backend). These are only injected during deployment. For the backend part this is usually done via environment variables. In the frontend, it is recommended to work with placeholders that are replaced before uploading to a CDN, as seen in the demo microfrontend.