Microfrontends in Depth: Part 3 of *

Luis Atencio
8 min readDec 27, 2023

--

In previous posts (parts 1 and 2) we focused on team structure and a set of best practices that can be shared amongst the teams collaborating on a microfrontend-based application. At the end of the day, the sole purpose of microfrontends is to allow teams to move faster and more independently.

Now let’s sink our teeth a little more into this architectural style. After we’ve identified the teams, and the requirements of what we’re trying to build are clear, we need to decide on the composition or integration pattern to use. In other words, we need to decide how the heck we’ll join all these pieces together so that it all looks like one giant Single Page App.

In this post, I’ll offer some high level designs (ideas) of different ways we can do this. In reality, a lot of this will be driven by how your company is structured, the technology stack (already being) used, or even the libraries that you or your team decide to use. In the following sections, I’ll shed some light at different architectural styles you can use to stitch together your application.

Let’s start with server-side composition.

Server-Side Composition (SSC)

This architecture has most or all components being stitched together at “the server.” The server in this case could be some specialty microservice or just simply a reverse proxy server (Apache, Nginx, etc) that supports Server-Side Includes (SSI). Here’s a quick look:

As you can see from this diagram, the web server (middle) in in charge of pulling down the different MFEs from either a dedicated backend or a CDN. Whether you use a dedicated backend for each MFE or not, will depend on how the microfrontend was implemented. Often it could be an existing legacy UI carved out to fit on a section of the page using PHP, or perhaps with React with Server-Side Components to generate the final HTML and JS.

When using CDNs, the most common scenario is to upload all of your assets: HTML + JS + CSS + images to the CDN storage servers (blob store in the case of Azure or S3 buckets for AWS). Most CDNs are really easy to configure in terms of caching, compression, global availability, and others. So instead of generating the HTML using PHP, for example, suppose you are using React. With React, you can use Webpack to bundle together all of your assets into a single bundle or emit them as separate bundles before uploading. All of the assets emitted from your build process are then uploaded and served directly from your CDN.

You’ve probably heard of Server-Side Rendering (SSR) before. Seems to be the talk of the town in the React world (18 adds tons of optimizations to support SSR). But you might not have heard of Server-Side Includes (SSI). Server-Side Includes (or SSI) is very similar in spirit to templating libraries like EJS or Mustache (or if you were a Java person you might be familiar with Freemarker, Velocity, or even good ol’ JSPs!). It is a set of directives (markup) that allow you to insert dynamic content to generate HTML pages. As the name suggests, you can include dynamic pieces of HTML fragments coming from any location (CDN or server-generated). Here’s an example:

<my-element data-arg1="hello">
<!--#include virtual="https://...example.com?q=world" -->
</my-element>

The <!--#include directive is parsed and replaced by the contents of the HTML generated from its virtual host.

You may even have multiple of these on the same page and so in this architecture, SSI’s become layout tags that govern the placement of all MFEs on the page — kind of like a layout engine.

For Server-Side Composition (SSC) to work, you don’t necessarily need have to an Apache or Nginx server. Alternatively, you could create your own layout microservice using any template technology of your choice and achieve the same purpose. What’s worth noticing is that Server-Side Composition is optimal when performance is a key requirement, or in rare cases where JavaScript is disallowed on the page. This is because database calls occur locally as the page is being built server-side, avoiding roundtrip calls from the user’s browser client to your database servers. Hence, most of the MFE’s assets and data is sent as a single payload to the browser, then you use JavaScript (if allowed) to hydrate the page and handle most of the user’s interactions.

Client-Side Composition (CSC)

By far the most popular approach, Client-Side Composition (or CSC) uses the browser as the hub to achieve composition. This pattern is much more popular because it more closely resembles a typical Single-Page App (SPA) architecture, where the browser is in charge of mounting and unmounting DOM elements dynamically, and browser APIs (e.g. History API) are used for routing and other tasks.

We’ll spend some time on this one, since it’s what you’ll most likely encounter in the real world. Here’s another simple diagram to illustrate:

At a high level, this looks a lot simpler because there’s no web or reverse-proxy server in between. In reality, all of the orchestration logic that otherwise lived in each web server must now be available using JavaScript and browser APIs. Just like web servers support a mechanism to stitch pages together, there are libraries available (not surprising in the JS world) that will make composition simpler and allow you to manage the lifecycle of the different MFEs that are being composed together. Some of the most popular include: Piral, single-spa, Podium, and others — some of these also support SSR.

In this post I won’t spend to much time on the libraries and how composition works at a low level. This will be separated as another blog post as we continue to dive deeper. For now, we’ll look at the high level principles based on the single-spa library. At its core, single-spa is a basic MFE router. It maps an “application” (an MFE) to a route, and takes charge of mounting and unmouting these applications as the user navigates to different routes on the page. Using the single-spa client libraries (e.g. single-spa-react, single-spa-angular, etc) you create the necessary hooks to automate most of the tasks of bootstrapping, mounting, and unmounting MFEs for the different view libraries, respectively. Here’s an example using React:

Again, we’re not discussing how single-spa glues MFEs together just yet, but picture an MFE as basically just a JavaScript module exposing an API. This API is consists of three lifecycle functions or hooks: bootstrap, mount, and unmount, pretty much in that order. bootstrap is used once to preload any data the MFEs need to initialize, such as the set of feature flags to use. mount is called to render the dynamically generated DOM to the page. Finally, unmount is called when the user navigates away from the current route (e.g. /myReactApp in this case) and into another application’s route. During an unmount is when you’ll want to perform clean-up tasks like removing outstanding event listeners or cleaning up objects from local storage.

With single-spa you register each application (MFE) with the route that activates it. This activates the layout engine which is just a very simple router in charge of mounting and unmounting code. However, the actual loading of the MFE assets occurs using the popular module loader library SystemJS. SystemJS supports Import Maps. Just like package.json describes the dependencies needed for any Node.js application, import maps describe where each MFE is located. Here’s a quick example:

{
@myOrg/myReactApp: "https://path-to-cdn.io/mfes/myReactApp1",
@myOrg/myReactApp2: "https://path-to-cdn.io/mfes/myReactApp2",
...
}

In a normal situation, a registered MFE such as “@myOrg/myReactApp1” mapped to the route /myReactApp is dynamically loaded and mounted when this path is reached.

Client-Side composition has very little overhead and smaller barrier to entry compared to its Server-Side counterpart. However, it does have a drawback. When the UI is rendered on the server (as with SSC), you are free to make service-to-service API calls to multiple microservices or even database servers directly, all potentially hosted in different domains owned by the different teams. However, when composition is done on the client, the browser is strict about enforcing Cross-Origin Resource Sharing (CORS). Talking to an API from the browser, will involve configuring CORS properly for each of the backend microservices. One way to overcome this is to configure all backends to be hosted under the same top-level domain. This is feasible for small organizations, but very hard to do for bigger organizations, especially when those backends are already in production. To overcome this, let’s discuss a slight variation to client-side composition, this time adding an API Gateway.

Client-Side Composition (CSC) with an API Gateway

To get around having to configure CORS and also having to maintain complicated Content Security Policy (CSP) headers to whitelist the myriad of backend services involved, you could develop a single, dedicated backend with the sole purpose of proxying all of the API calls needed to support all of the MFEs.

This API proxy or gateway doesn’t necessarily need to expose all of your organization’s data, just the necessary bits required to assemble this particular UI. This pattern is called a Backend For Frontends (BFF). Another benefit of doing this is that you can normalize the domain models of each of the different API backends built by the different teams, and translate them into a single cohesive and consistent API and data model.

My technology of choice here, and again this is subjective, is to use GraphQL for the API proxy. GraphQL provides a simple, standardized, query language to easily discover all of your data. GraphQL supports different types of resolvers so that you can abstract GraphQL queries over data coming from either databases (SQL or no-SQL), gRPC, file system, and other RESTful APIs.

I hope this post gave you a high-level idea of where to begin designing your MFE-based application. I encourage you to research all of the different tools and patterns discussed here to gain a more complete picture. There are other architectural styles and different tools out there, but in principle most will fall under either Client-Side or Server-Side composition. Thanks for reading and stay tuned for Part 4!

--

--

Luis Atencio

Author of Functional Programming in JavaScript, RxJS in Action, and The Joy of JavaScript