Micro Frontend Architecture

for

Facile.it

A frontend architectural analysis and implementation to introduce a micro frontend solution tailored to Facile's business needs.

INDUSTRY
Fintech
SERVICES
Sviluppo software personalizzato
TECHNOLOGIES
Next.js
React JS

We develop top-notch custom enterprise software tailored to our clients’ specific requirements.

Contact us

Facile.it features several landing pages to promote its main products (insurance, mortgages, etc.). These pages are composed of several “blocks” containing static information from a CMS. They then link to interactive sections of the website where users can enter their information to receive a quote.

For UX and SEO reasons, Facile.it wanted to allow users to start interacting directly from the landing pages, integratingdynamic forms within the landing page blocks.

However, this posed a challenge: the landing pages and the dynamic pages are managed by different teams. How couldone team “provide” a dynamic section so that it could be included in the landing page by another team?

The solution we needed allow all teams to independently evolve their own part of the product, while providing flexible integration points.

Challenge

Micro frontends should solve this, right?

To recap, we have:

  • One team that manages the landing pages, which are obtained by assembling static blocks from the CMS and, if we are successful, dynamic blocks from other teams.
  • Several teams that manage what we call “dynamic” pages, i.e., interactive pages linked from the landing pages. These teams want to integrate some blocks of their pages into the landing pages (e.g., a form to collect some initial data from the user).

Micro frontends immediately seemed like an obvious solution to the problem: we have multiple “frontends” to integrate, managed by different teams; that’s why this architectural pattern was born.

However, micro frontends come in many shapes and forms, and not all of them are suited for the specific business needs. Specifically, in the context of Facile.it, it was absolutely key to:

  • Server-render the entire page, including dynamic blocks.
  • Avoid introducing strong coupling between the team managing the landing pages and those responsible for the dynamic pages.

For example, many popular micro frontend solutions, such as single-spa,, require strong coordination between front-end applications, e.g., by enforcing the use of a specific build tool like Webpack. The Facile.it team initially explored this solution, but opted against it due to the technical constraints it would have imposed on the development teams.

Outcome

A tailor-made architecture

After an initial analysis, Buildo envisioned a new architecture that builds on several proven technologies combined in a novel way—something that didn't exist off the shelf and was perfectly tailored for Facile.it's business needs.

We then built a working MVP of the solution: we did not stop at prototyping; instead, we worked directly with Facile.it’s technical team to deliver value immediately.

We documented the solution, created specific artifacts to accelerate adoption across other internal teams, and supported Facile.it’s tech leads in making the new solution production-ready.

Methodology

Micro frontends without a framework

Constraints and challenges

Adding to what we discussed above, we benefited from specific technical constraints that limited the scope of our analysis:

  • All teams at Facile.it use React.js.
  • Almost all teams use Next.js.
  • All teams share a Design System library (although they might be on different versions).

The main challenges we wanted to address were:

  • Integrating a “dynamic block” (i.e., a standalone React application) within the landing page (which is a Next.js app).
  • Making the “dynamic block” available from another Next.js app, sharing the existing infrastructure. In other words: the same React component had to be usable directly within the host Next.js app or served as a standalone block for inclusion into the landing page.
  • Achieving all of the above while preserving Server Side Rendering (SSR).

Secondary challenges were:

  • Avoiding duplication whenever possible: e.g., preventing the inclusion of the same version of React multiple times on the page (the same applied to other shared libraries like the design system).
  • At the same time, we did not want to impose specific library versions on any team. In other words, we wanted to allow teams to use different versions of React, the Design System, etc., but enable deduplication if they happened to use the same version.

Serving the dynamic block

Due to its architecture, Next.js can only output entire applications that take over the entire dom.

We then needed a way to bundle a single component and its related assets and serve them via Next.js.

The solution we eventually landed on uses Vite with a custom backend integration: this way we could “package” existing React components into standalone applications that could be server-side rendered and then served over the network.

Once we validated that the solution was working, we could package everything into a custom Vite plugin to hide the complexity and make it straightforward for each team to configure. The resulting Vite configuration would look something like this:

// vite.config.ts

import { defineConfig } from 'vite'
import { facileItMarkup } from '@facile-it/markup-vite-plugin'

export default defineConfig({
    plugins: [
        facileItMarkup({
            blockIds: ['my-dynamic-block'],
        })
    ],
})

Similarly, we provided a utility library to serve the component using Next.js’s route handlers, and again, our target was to make the integration as concise as possible, without leaking the internal complexity:

// src/pages/api/markup/my-dynamic-block/index.ts

import { makeMarkupHandler } from '@facile-it/markup-utils/server'
import { render } from './entry-server'

export default makeMarkupHandler(
    req => {
        // Parse the request and return the parameters needed by the render function, if any.
        // You can return an empty object or null if no parameters are needed.
        return { someParam: req.query.someParam, someOtherParam: req.query.someOtherParam }
    },
    params => render(params, blockId).html,
    'my-dynamic-block'
)

Integrating the dynamic block in the landing pages

In the Next.js app rendering the landing pages, we retrieved the URL of the dynamic block from the CMS and “materialized” it by fetching its text content.

This gives us the server-side rendered HTML of the block, which we then can include in our own landing page using the dangerouslySetInnerHTML functionality from React:

// __html here is the server-side rendered content retrieved by the block URL
return <div dangerouslySetInnerHTML={{ __html }} suppressHydrationWarning />

This way, the HTML content of the dynamic block is inserted right away during the first server-side render of the landing page, satisfying our main SEO requirement.

Avoiding duplications

Including multiple dynamic blocks without a bundling step presented another challenge: each block, as well as the host application, would include its own version of React and other common libraries, such as the design system component library.

Our two goals here were:

  • Avoid including the same version of a dependency twice in the page.
  • Avoid forcing a specific version of a dependency to all teams, since this would introduce the need for coordination across teams, which chips away at the benefits of working independently on separate parts of the frontend.

The solution we landed on leverages the import module, which is widely available across major browsers.

The general intuition is that instead of bundling the shared dependency, the host application and all the blocks would instead import the dependency from a common url, specifically an ESM-capable CDN, like https://esm.sh.

Concretely, this means applying a transform step to both blocks and host apps such that an import like

import { useState } from 'react';

would become

import { useState } from 'https://esm-cdn.facile.it/react@18.3.1'

The interesting bit to notice here is that browsers automatically cache and de-duplicate imports from the same module, so we gain these properties:

  • If two dynamic blocks import the same version of a library, this will result in only one network call.
  • If two dynamic blocks import two different versions, this will result in two network calls.

This meets our goals: optimize whenever possible, while still allowing flexibility to the different teams.

Conclusion

Maximizing the business needs via tailor-made solutions

At Buildo, we always look beyond technical trends and focus on our clients' business needs; this allows us to craft tailor-made solutions that truly meet their goals.

In this case, we chose to discard well-established solutions like Single SPA or Module Federation as they would have introduced too many compromises for Facile.it. Instead, we used our deep knowledge of frontend architectures to compose lower-level components into a well-crafted solution that’s tailored to their specific requirements.

The solution has since been fully integrated, released to production, and is performing as envisioned, which is ultimately the best metric for determining its success.

Still curious? Dive deeper

No items found.

Mettiamoci al lavoro!

Stai cercando un partner affidabile per sviluppare la tua soluzione software su misura? Ci piacerebbe sapere di più sul tuo progetto.