6 Patterns for Microfrontends

An overview of microfrontend patterns: advantages, drawbacks and implementation.

Image by Michael Gaida from Pixabay

Microfrontends are not a new thing, but certainly a recent trend. Coined in 2016, the pattern slowly gained popularity as problems started to appear when developing large scale web apps.

In this article, we’ll go over the different patterns for creating microfrontends, their advantages and drawbacks, as well as implementation details and examples for each of the presented methods. I will also argue that microfrontends come with some inherited problems that may be solved by going even a step further — into a region that could be either called Modulith or Siteless UI, depending on the point of view.

But let’s go step by step. We start our journey with a historic background.

Background

When the web (i.e., HTTP as transport and HTML as representation) started there was no notion of “design” or “layout”. Instead, text documents have been exchanged. The introduction of the <img> tag changed that all. Together with <table> designers could declare war on good taste. Nevertheless, one problem arises quite quickly: How is it possible to share a common layout across multiple sites? For this purpose two solutions have been proposed:

Use a program to dynamically generate the HTML (slowish, but okay — especially with the powerful capabilities behind the CGI standard) Use mechanisms already integrated into the web server to replace common parts with other parts

While the former lead to C and Perl web servers, which became PHP and Java then converted to C# and Ruby, before finally emerging at Elixir and Node.js, the latter wasn’t really after 2002. The web 2.0 also demanded more sophisticated tools, which is why server-side rendering using full-blown applications dominated for quite while.

Until Netflix came and told everyone to make smaller services to make cloud vendors rich. Ironically, while Netflix would be ready for their own data centers they are still massively coupled to cloud vendors such as AWS, which also hosts most of the competition including Amazon Prime Video.

The Patterns

In the following, we’ll look at some of the patterns that are possible for actually realizing a microfrontend architecture. We’ll see that “it depends” is in fact the right answer when somebody asks: “what is the right way to implement microfrontends?”. It very much depends on what we are after.

Each section contains a bit of example code and a very simple snippet (sometimes using a framework) to realize that pattern for a proof of concept or even an MVP. In the end, I try to provide a small summary to indicate the target audience according to my personal feelings.

Regardless of the pattern you choose, when integrating separate projects, keeping a consistent UI is always a challenge. Use tools like Bit (Github) to share and collaborate on UI components across your different microfrontends.

Sharing components across projects with Bit

The Web Approach

The most simple method of implementing microfrontends is to deploy a set of small websites (ideally just a single page), which are just linked together. The user goes from website to website by using the links leading to the different servers providing the content.

To keep the layout consistent a pattern library may be used on the server. Each team can implement the server-side rendering as they desire. The pattern library must also be usable on different platforms.

Pattern: Web Approach

Using the web approach can be as simple as deploying static sites to a server. This could be done with a Docker image as follows:

Obviously, we are not restricted to use a static site. We can apply server-side rendering, too. Changing the nginx base image to, e.g., ASP.NET Core allows us to use ASP.NET Core for generating the page. But how is this different to the frontend monolith? In this scenario, we would, for example, take a given microservice, exposed via a web API (i.e., returning something like JSON) and change it to return rendered HTML instead.

Logically, microfrontends in this world are nothing more than a different way of representing our API. Instead of returning “naked” data we generate the view already.

In this space we find the following solutions:

What are pros and cons of this approach?

Pro : Completely isolated

: Completely isolated Pro : Most flexible approach

: Most flexible approach Pro : Least complicated approach

: Least complicated approach Con : Infrastructure overhead

: Infrastructure overhead Con : Inconsistent user experience

: Inconsistent user experience Con: Internal URLs exposed to outside

Server-Side Composition

This is the true microfrontend approach. Why? As we have seen, microfrontends have been supposed to be run server-side. As such the whole approach works independently for sure. When we have a dedicated server for each small frontend snippet we may really call this micro frontend.

In a diagrammatic form, we may end up with a sketch like the one below.

Pattern: Server-Side Composition

The complexity of this solution lies totally in the reverse proxy layer. How the different smaller sites are combined into one site can be tricky. Especially things like caching rules, tracking, and other tricky bits will bite us at night.

In some sense, this adds a kind of gateway layer to the first approach. The reverse proxy combines different sources into a single delivery. While the tricky bits certainly need to (and can) be solved somehow.

A little bit more powerful would be something like using Varnish Reverse Proxy.

In addition, we find that this is also a perfect use case for ESI (abbr. Edge-Side Includes) — which is the (much more flexible) successor to the historic Server-Side Includes (SSI).

A similar setup can be seen with the Tailor backend service, which is a part of Project Mosaic.

In this space we find the following solutions:

What are pros and cons of this approach?

Pro : Completely isolated

: Completely isolated Pro : Looks embedded to user

: Looks embedded to user Pro : Very flexible approach

: Very flexible approach Con : Enforces coupling between the components

: Enforces coupling between the components Con : Infrastructure complexity

: Infrastructure complexity Con: Inconsistent user experience

Client-Side Composition

At this point one may be wondering: Do we need the reverse proxy? As this is a backend component we may want to avoid this altogether. The solution is client-side composition. In the simplest form this can be implemented with the use of <iframe> elements. Communication between the different parts is done via the postMessage method.

Pattern: Client-Side Composition

Note: The JavaScript part may be replaced with “browser” in case of an <iframe> . In this case the potential interactivity is certainly different.

As already suggested by the name, this pattern tries to avoid the infrastructure overhead coming with a reverse proxy. Instead, since microfrontends contain already the term “frontend” the whole rendering is left to the client. The advantage is that starting with this pattern serverless may be possible. In the end the whole UI could be uploaded to, e.g., a GitHub pages repository and everything just works.

As outlined, the composition can be done with quite simple methods, e.g., just an <iframe> . One of the major pain points, however, is how such integrations will look to the end user. The duplication in terms of resource needs is also quite substantial. A mix with pattern 1 is definitely possible, where the different parts are being placed on independently operated web servers.

Nevertheless, in this pattern knowledge is required again — so component 1 already knows that component 2 exists and needs to be used. Potentially, it even needs to know how to use it.

Considering the following parent (i.e., delivered application or website):

https://gist.github.com/dddbf80773ed8df03fcf20679f30c835

We can write a page that enables the direct communication path:

If we do not consider frames an option we could also go for web components. Here, communication could be done via the DOM by using custom events. However, already at this point in time it may make sense to consider client-side rendering instead of client-side composition; as rendering implies the need for a JavaScript client (which aligns with the web component approach).

In this space we find the following solutions:

What are pros and cons of this approach?

Pro : Completely isolated

: Completely isolated Pro : Looks embedded to user

: Looks embedded to user Pro : Serverless possible

: Serverless possible Con : Enforces coupling between the components

: Enforces coupling between the components Con : Inconsistent user experience

: Inconsistent user experience Con: May require JavaScript / no seamless integration

Client-Side Rendering

While client-side composition may work without JavaScript (e.g., only using frames that do not rely on communication with the parent or each other), client-side rendering will fail without JavaScript. In this space we already start creating a framework in the composing application. This framework has to be respected by all microfrontends brought in. At least they need to use it for being mounted properly.

The pattern looks as follows.

Pattern: Client-Side Rendering

Quite close to the client-side composition, right? In this case the JavaScript part may not be replaced. The important difference is that server-side rendering is in general off the table. Instead, pieces of data are exchanged, which are then transformed into a view.

Depending on the designed or used framework the pieces of data may determine the location, point in time, and interactivity of the rendered fragment. Achieving a high degree of interactivity is no problem with this pattern.

In this space we find the following solutions:

What are pros and cons of this approach?

Pro : Enforces separation of concerns

: Enforces separation of concerns Pro : Provides loose coupling of the components

: Provides loose coupling of the components Pro : Looks embedded to the user

: Looks embedded to the user Con : Requires more logic in the client

: Requires more logic in the client Con : Inconsistent user experience

: Inconsistent user experience Con: Requires JavaScript

SPA Composition

Why should we stop at client-side rendering using a single technology? Why not just obtain a JavaScript file and run it besides all the other JavaScript files? The benefit of this is the potential use of multiple technologies side-by-side.

Pattern: SPA Composition

It is up for debate if running multiple technologies (independent if its in the backend or the frontend — granted, in the backend it may be more “acceptable”) is a good thing or something to avoid, however, there are scenarios where multiple technologies need to work together.

From the top of my head:

Migration scenarios

Support of a specific third-party technology

Political issues

Team constraints

Either way, the emerged pattern could be drawn as below.

So what is going on here? In this case delivering just some JavaScript with the app shell is no longer optional — instead, we need to deliver a framework that is capable of orchestrating the microfrontends.

The orchestration of the different modules boils down to the management of a lifecycle: mounting, running, unmounting. The different modules can be taken from independently running servers, however, their location must be already known in the application shell.

Implementing such a framework requires at least some configuration, e.g., a map of the scripts to include:

The lifecycle management may be more complicated than the script above. Thus a module for such a composition needs to come with some structure applied — at least an exported mount and unmount function.

In this space we find the following solutions:

What are pros and cons of this approach?

Pro : Enforces separation of concerns

: Enforces separation of concerns Pro : Gives developers much freedom

: Gives developers much freedom Pro : Looks embedded to the user

: Looks embedded to the user Con : Enforces duplication and overhead

: Enforces duplication and overhead Con : Inconsistent user experience

: Inconsistent user experience Con: Requires JavaScript

Siteless UIs

This topic deserves its own article, but since we are listing all the patterns I don’t want to omit it here. Taking the approach of SPA composition all we miss is a decoupling (or independent centralization) of the script sources from the services, as well as a shared runtime.

Both things are done for a reason:

The decoupling makes sure that UI and service responsibilities are not mixed up; this also enables serverless computing

The shared runtime is the cure for the resource intense composition given by the previous pattern

Both things combined yield benefits to the frontend as “serverless functions” did for the backend. They also come with similar challenges:

The runtime cannot be just updated — it must stay / remain consistent with the modules

Debugging or running the modules locally require an emulator of the runtime

Not all technologies may be supported equally

The diagram for siteless UIs looks as follows.

Pattern: Siteless UIs

The main advantage of this design is that sharing of useful or common resources is supported. Sharing a pattern library makes a lot of sense.

All in all the architecture diagram looks quite similar to the SPA composition mentioned earlier. However, the feed service and the coupling to a runtime bring additional benefits (and challenges to be solved by any framework in that space).

The big advantage is that once these challenges are cracked the development experience is supposed to be excellent. The user experience can be fully customized, treating the modules as flexible opt-in pieces of functionality. A clear separation between feature (the respective implementation) and permission (the right to access the feature) is thus possible.

One of the easiest implementations of this pattern is the following:

This uses a global variable to share the API from the app shell. However, we already see several challenges using this approach:

What if one module crashes?

How to share dependencies (to avoid bundling them with each module, as shown in the simple implementation)?

How to get proper typing?

How is this being debugged?

How is proper routing done?

Implementing all of these features is a topic of its own. Regarding the debugging we should follow the same approach as all the serverless frameworks (e.g., AWS Lambda, Azure Functions) do. We should just ship an emulator that behaves like the real thing later on; except that it is running locally and works offline.

In this space we find the following solutions:

What are pros and cons of this approach?

Pro : Enforces separation of concerns

: Enforces separation of concerns Pro : Supports sharing of resources to avoid overhead

: Supports sharing of resources to avoid overhead Pro : Consistent and embedded user experience

: Consistent and embedded user experience Con : Strict dependency management for shared resources necessary

: Strict dependency management for shared resources necessary Con : Requires another piece of (potentially managed) infrastructure

: Requires another piece of (potentially managed) infrastructure Con: Requires JavaScript

A Framework for Microfrontends

Finally, we should have a look at how one of the provided frameworks can be used to implement microfrontends. We go for Piral as this is the one I’m most familiar with.

In the following we approach the problem from two sides. First, we start with a module (i.e., microfrontend) in this context. Then we’ll walk over creating an app shell.

For the module we use my Mario5 toy project. This is a project that started several years ago with a JavaScript implementation of Super Mario called “Mario5”. It was followed up by a TypeScript tutorial / rewrite named “Mario5TS”, which has been kept up-to-date since then.

For the app shell we utilize the sample Piral instance. This one shows all concepts in one sweep. It’s also always kept up-to-date.

Let’s start with a module, which in the Piral framework is called a pilet. At its core, a pilet has a JavaScript root module which is usually located in src/index.tsx.

A Pilet

Starting with an empty pilet gives us the following root module:

We need to export a specially named function called setup . This one will be used later to integrate the specific parts of our application.

Using React we could for instance register a menu item or a tile to be always displayed:

Since our tile requires some styling we also add a stylesheet to our pilet. Great, so far so good. All the directly included resources will be always available on the app shell.

Now its time to also integrate the game itself. We decide to put it in a dedicated page, even though a modal dialog may also be cool. All the code sits in mario.ts and works against the standard DOM — no React yet.

As React supports also manipulation of hosted nodes we use a reference hook to attach the game.

Theoretically, we could also add further functionality such as resuming the game or lazy loading the side-bundle containing the game. Right now only sounds are lazy loaded through calls to the import() function.

Starting the pilet will be done via

https://gist.github.com/0678ac99f4653843159cf8e54ad06422

which uses the Piral CLI under the hood. The Piral CLI is always installed locally, but could be also installed globally to get commands such as pilet debug directly available in the command line.

Building the pilet can be also done with the local installation.

The App Shell

Now its time to create an app shell. Usually, we would already have an app shell (e.g., the previous pilet was created already for the sample app shell), but in my opinion its more important to see how development of a module goes.

Creating an app shell with Piral is as simple as installing piral . To make it even more simple the Piral CLI also supports scaffolding of a new app shell.

Either way we will most likely end up with something like this:

Here we do three things:

We set up all the imports and libraries We create the Piral instance; supplying all functional options (most importantly declaring where the pilets come from) We render the app shell using components and a custom layout that we defined

The actual rendering is done from React.

Building the app shell is straight forward — in the end its a standard bundler (Parcel) to process the whole application. The output is a folder containing all files to be placed on a webserver or static storage.

Points of Interest

Coining the term “Siteless UI” potentially requires a bit of explanation. I’ll try to start with the name first: As seen, it is a direct reference to “Serverless Computing”. While serverless may also be a good term for the used technology, it may also be misleading and wrong. UIs can generally be deployed on serverless infrastructures (e.g., Amazon S3, Azure Blob Storage, Dropbox). This is one of the benefits of “rendering the UI on the client” instead of doing server-side rendering.

However, I wanted to follow the approach “having an UI that cannot live without a host” kind a thing. Same as with serverless functions, which require a runtime sitting somewhere and could not start otherwise.

Let’s compare the similarities. First, let’s start of with the conjunction that microfrontends should be to frontend UIs what microservices have been to backend services. In this case we should have:

can start standalone

provide independent URLs

have independent lifetime (startup, recycle, shutdown)

do independent deployments

define an independent user / state management

run as a dedicated webserver somewhere (e.g., as a docker image)

if combined, we use a gateway-like structure

Great, certainly some things that apply here and there, however, note that some of this contradicts with SPA composition and, as a result, with siteless UIs, too.

Now let’s compare this to an analogy that we can do if the conjecture changes to: siteless UIs should be to frontend UIs what serverless functions have been to backend services. In this case we have:

require a runtime to start

provide URLs within the environment given by the runtime

are coupled to the lifetime defined by the runtime

do independent deployments

defined partially independent, partially shared (but controlled / isolated) user / state management

run on a non-operated infrastructure somewhere else (e.g., on the client)

If you agree that this reads like a perfect analogy — awesome! If not, please provide your points in the comments blow. I’ll still try to see where this is going and if this seems like a great idea to follow. Much appreciated!

Further Reading

The following posts and articles may be useful to get a complete picture:

Conclusion

Microfrontends are not for everyone. Hell, they are not even for every problem. But what technology is? They certainly fill a spot. Depending on the specific problem one of the patterns may be applicable. It’s great that we have many diverse and fruitful solutions available. Now the only problem is to choose — and choose wisely!

Learn More