Getting Started with Micro Frontends

What are Micro Frontends?

With most of the current state of development of a web application, we generally see that it's a feature-rich and powerful application, and it's called a Single Page App or SPA. These types of applications are made using a single frontend framework or library like Angular, React, etc.

But over time, as we see all around, the application in itself grows more in many features and complexity and, in turn, can be challenging to maintain, especially when a separate team develops it. This is what we call a monolith approach.

On the other hand, we have the approach to using Micro Frontend architectures. Simply put,

Micro Frontends are a pattern where web applications User Interfaces (UIs) or a frontend are composed of independent or semi-independent fragments that can be built by different teams using different technologies or tech stacks.

With a micro frontend, you can have a modularized structure of a microservice backend. Thus, micro frontends resemble backend architectures where each backend is composed of semi-independent/independent microservices.

Architecture of Micro Frontends

It's an architectural style of building more robust web apps where independently deliverable frontends are composed into a more significant whole unit. These micro frontends extend the ability to make web applications as separate parts of a whole, right from the database all the way up to the raw HTML.

Benefits and Challenges to Micro Frontends

Recently, "micro frontends" has become a buzzword in tech, and for the right reasons, it changes the way developers think about web application development architectures.

In this section, we will deal with what benefits you get by using micro frontends and what are some of the disadvantages or challenges that it faces today in real-world apps.

Benefits of Micro Frontends

Some of the top benefits on why you should choose them are:

  1. Iterative approach to app updates: micro frontends aim at decreasing the size and prominence of the traditional monolith architecture for greater update flexibility. These micro frontends can maintain backend functionalities without much friction, and it doesn't affect the other parts of the code.

    With this, organizations can move quickly and effectively in their app update cycles without external side effects. The User Experience (UX) is also improved as micro frontends can break large-scale workflow systems into smaller components. From there, creating, scaling, and updating any business logic becomes easier for an improved end consumer experience.

    Hence, they prove to be incredibly useful with more iterative updates, which in turn decreases the cost and complexity of the whole process.

  2. Independent deployment and testing: when you work with a micro frontend, they are independent; this means that deployment and other changes will affect only that specific microservice used in that change. It doesn't cause any change to the whole app.

    By this, the development and testing teams can focus only on monitoring one aspect of the whole app while avoiding the need for the entire project to be tested or monitored at the same time.

  3. Helps to develop different views of your processes relevant to each person’s role: using micro frontends, you can quickly create different views for each person's unique role in a process-driven application.

    Traditionally, when you build SPAs, you create one app for each user role. But a micro frontend architecture enables you to make changes quickly for any number of different roles. Also, it becomes easier to update these elements as your app responds to the user's needs and business logic.

  4. Makes your web application more maintainable: if you're a seasoned developer using the monolith approach, you would know that testing and maintaining large applications is really hard, challenging, and take a lot of time.

    But as we know micro frontend approach is like divide and conquer; by choosing this architecture for your next web application, you can easily make your business requirements easier to test and maintain.

    By having smaller chunks of application, it's much more affordable to understand the flow of what's happening, making them more reliable and easy to maintain by teams.

  5. Allows you to democratize user experience: like microservices democratized backend development workflow, micro frontends allow to enable this service-oriented culture to extend to all parts of the development lifecycle.

    It allows both the frontend and the backend teams to iterate independently at their own pace rather than being held up by organizational barriers. This decreases the bottlenecks in favor of better communication between developers allowing cross-functional teams to work autonomously.

Challenges of Micro Frontends

These are the five challenges to keep in mind:

  1. Complexity: with too many components to break down in an application, developers may overpopulate their project as time goes in building the app. This causes multiple testing to be done and issues to occur while deploying across several teams.

    Without detailed planning of what type of components to employ and how many of them, the process can become complex if not handled well. All of this costs more time on management and resources.

  2. Workflows crossing boundaries: it can be very challenging to both create and maintain workflows shared between micro frontends and the shell.

    Different teams can update each application and, therefore, can be released and changed independently. But suppose in your web app, rather than using the built-in navigation utilities, you build your custom navigation system that adds to the complexity. For this, you must pass the app state across separate applications and check who is responsible for saving the complete app state.

    If you make a change to one of your frontends, you will need to test that all of the connected multi-workflows are still intact and working as expected. You end up having test version checks all over the place.

  3. Payload: the issue of payload comes into the picture once we get to see that if a micro frontend requires a specific program/library to be installed client side in order to function, then the client also needs to download its corresponding copy as a payload while accessing the application.

    This problem worsens when each component must ensure multiple payloads are downloaded for proper browser support. Anytime extra data demand is a disadvantage as the user may not return back to using your app, having poor performance.

  4. Reducing discoverability leads to duplicate implementations: with the approach of splitting an application, the ability to discover existing code may go down. Next, you need to search deep in your project's codebase for what pieces of code are to be reused.

    While working with teams, refactoring becomes an issue as you don't want to be responsible for refactoring a code of an unfamiliar project to get access to a shared component.

    All this leads to duplicate implementations across separate frontends. And as you may know, having duplicate components means higher costs over time. Future changes will now require more significant changes in diverse places of your app, and ultimately this leads to a buggy application.

  5. Environment differences: we would usually strive to develop a single micro frontend without thinking about all of the other ones being developed by other teams. This might make development more straightforward, but there are certain risks associated while growing in an environment that is starkly different from the production one.

    If our development container behaves differently than the production one, we may find that our micro frontend is either broken or doesn’t perform as expected. For example, the global styles brought along by the container or other micro frontends might be very different.

Comparing Micro Frontends Solutions

With the rise of micro frontends, we also see that many solutions are coming up to tackle particular challenges, as discussed above.

Some of these solutions give you smart build-time integrations of components, and some provide you with dynamically imported code from another app. In this section, let's take a look at three of the major solutions we currently have for micro frontends:

1) Single SPA: in short Single SPA is:

A JavaScript router for frontend microservices.

It is a framework for bringing together multiple JavaScript micro frontends in a frontend application and is mostly concerned about cross-framework components. This means you use multiple frameworks, like React, Angular, Ember, etc., on the same page without refreshing the page.

It applies a lifecycle to every application. In this, each app responds to URL routing events and must know how to mount/unmount itself from the DOM. Single SPA is best suitable if you want to put together different frontends/frameworks into one DOM to integrate at runtime.

2) Module Federation: this is built on the principle that:

Multiple separate builds should form a single application. These different builds should not have dependencies on each other, so they can be developed and deployed individually.

Generally, Module Federation only takes care of dependency sharing and is heavily tooling dependent. For example, once you're downloading a React component, your app will not import the React code twice once it's loaded, and it will use the source you already downloaded and then only import the component code.

The above two frameworks we saw are strongly coupled, i.e., you cannot disable the micro frontend, and then you also get feature overlap where one micro frontend depends on a specific version of another.

3) Piral: Piral is a framework for next-gen portal applications.

It allows you to quickly build web apps that follow the micro frontends architecture.

Piral takes care of everything you need in order to create distributed web applications with the flexibility and modularized structure of a microservice backend.

It helps you to create a modular frontend application extended at runtimes and comes with decoupled modules called 'pilets.' A pilet can be used to:

  • Bring the functionality, queries, and mutations for the application.
  • Include your own assets and dedicated dependencies.
  • Define where you want to integrate the components.

Piral's application shell is called a Piral instance, which:

  • Brings the overall design of the application (e.g., header, footer, navigation, etc.)
  • Includes shared components that can be used by pilets.
  • Defines how pilets are loaded and where pilets can integrate their components.

Here, Piral differs from the other two in the list as it is loosely coupled and lets you always deploy your micro frontend without depending on another micro frontend. It doesn't matter if it's always there; apps made with Piral always work.

Creating your first application with Piral

Getting started with Piral is actually quite smooth and easy. The documentation page has all the steps. Besides the option of starting with an app shell that holds together all micro frontends we can also directly start with micro frontend development for an existing app shell.

The team behind Piral created some ready-to-use app shells. One of these is the "sample-piral" app shell. Starting development for a micro frontend for this app requires only npm and Node.js. In the command line of a new directory run:

npm init pilet -- --source sample-piral --bundler esbuild --defaults

Remark: In order versions of npm (6) the forwarding dashes (--) can be left out.

Now the pilet should be ready. Let's have a look at the code. The most important file is the src/index.tsx. Here, everything comes together. Let's change the original code from the following:

import * as React from 'react';
import { PiletApi } from 'sample-piral';

export function setup(app: PiletApi) {
  app.showNotification('Hello from Piral!', {
    autoClose: 2000,
  });
  app.registerMenu(() =>
    <a href="https://docs.piral.io" target="_blank">Documentation</a>
  );
  app.registerTile(() => <div>Welcome to Piral!</div>, {
    initialColumns: 2,
    initialRows: 1,
  });
}

to only expose a page that is made visible via a link:

import * as React from 'react';
import { Link } from 'react-router-dom';
import { PiletApi } from 'sample-piral';

const MyPage = React.lazy(() => import('./Page'));

export function setup(app: PiletApi) {
  app.registerMenu(() =>
    <Link to="/my-page">My Page</Link>
  );
  app.registerPage("/my-page", MyPage);
}

In the code we are - besides the setup function itself - not using anything custom. Using Link and React.lazy are familiar to any developer of React. This is the basic philosophy behind Piral. Pilets should just use the concepts of the underlying frameworks. Therefore, no meta router like in single-spa is required. The only new concept is that components now need to be registered where / how they should be used.

The code for the page is in src/Page.tsx. It reads:

import * as React from 'react';
import { PageComponentProps } from 'sample-piral';

const Page: React.FC<PageComponentProps> = ({ piral: app }) => {
  return (
    <>
      <h1>My Page</h1>
      <p>This is some text.</p>
      <app.Extension name="important-info" />
      <p>Some more text.</p>
    </>
  );
};

export default Page;

The page is actually quite simple in its logic. But looking closely you see that we also placed one special element in there. The provided piral prop refers back to the API that allowed us to register pages and more earlier on. Now we can use the same API to create an extension slot.

An extension slot is a construct that is quite similar to what web components can offer. Let's say we have the following HTML code:

<h1>My Page</h1>
<p>This is some text.</p>
<x-important-info></x-important-info>
<p>Some more text.</p>

If some script is loaded that does a call to customElements.define with an element "x-important-info" then something will be shown. Otherwise, the spot may just remain empty.

Unfortunately, web components have quite a few downsides which make them less ideal for such placeholders:

  • They cannot be undefined, so no way to remove micro frontends cleanly
  • They cannot be defined multiple times, so no way of multiple micro frontends contributing to a placeholder
  • They are bound by the HTML model with string-based attributes, which does not work together so nicely with some UI frameworks such as React

Nevertheless, to actually illustrate what an extension slot is web components provide a useful model.

Let's start the micro frontend by running

npm start

which will - under the hood - run pilet debug. The browser will show a page like this:

For testing purposes we can also fill the placeholder spot. To do that we can actually register an extension ourselves in the src/index.tsx. Change it to be:

import * as React from 'react';
import { Link } from 'react-router-dom';
import { PiletApi } from 'sample-piral';

const MyPage = React.lazy(() => import('./Page'));

export function setup(app: PiletApi) {
  app.registerMenu(() =>
    <Link to="/my-page">My Page</Link>
  );
  app.registerPage("/my-page", MyPage);

  if (process.env.NODE_ENV === 'development') {
    app.registerExtension('important-info', () => (
        <p>
            <strong>WARNING</strong> Test extension
        </p>
    ));
  }
}

The browser should update automatically. The page now looks like this:

Great! So how do you bring such a micro frontend online? There are multiple ways, but the most straight forward way may be to use the official Piral Cloud feed service. This service is free for personal development purposes. You can just log in via an existing Microsoft account at feed.piral.cloud.

When clicking on "Create Feed" you can enter a new (unique) feed name and some details.

Now click create and finally you are on a page where the current pilets are shown. Right now we have none.

We can now either create an API key and publish the pilet from the command line, or we just upload the pilet via the web interface. Let's automate this right away by clicking on the symbol ("Manage API keys"), then clicking the button "Generate API Key".

Give the key a name, but leave the rest as is. The default scope ("pilets.write") is sufficient. Then click on "generate". Click on the generated key to copy it.

On the command line run from the pilet's directory:

npx pilet publish --fresh --api-key <copied-key> --url <feed-url>

where the copied-key part should be replaced by the key you copied. The feed-url must be replaced with the URL from your feed. In my case the command looked like:

npx pilet publish --fresh --api-key bd3e907b54c1b275cc... --url https://feed.piral.cloud/api/v1/pilet/vk-pilets

The portal should auto update and now list the pilet:

Wonderful - you've just published your first micro frontend. But how can you use it? After all, we don't have any page online. One thing we can do here is to just clone the repository of Piral - where the sample-piral app shell is developed. However, this is rather cumbersome and not great. The other thing is to just scaffold a new app shell and point it to the current feed.

In a new directory run:

npm init piral-instance -- --bundler esbuild --defaults

Once installed open the src/index.tsx in your text editor and change

const feedUrl = 'https://feed.piral.cloud/api/v1/pilet/empty';

to be

const feedUrl = 'https://feed.piral.cloud/api/v1/pilet/vk-pilets';

where the specific URL is the one you used to publish your pilet earlier. Now run npm start on the command line.

Even though the overall design is different we still get an entry in the menu (though looking a bit different, of course). And most importantly we still have the page. With this in mind let's stop the debugging, create and publish a second pilet, and see both coming together.

In a new directory, run again the npm init command for a pilet. Again, we can choose the sample-piral as app shell for trying / debugging the pilet. Change the src/index.tsx to be:

import * as React from 'react';
import { PiletApi } from 'sample-piral';

const MyExtension = React.lazy(() => import('./MyExtension'));

export function setup(app: PiletApi) {
  app.registerExtension("important-info", MyExtension);
}

The file src/MyExtension.tsx looks like this:

import * as React from "react";

function MyExtension() {
  const [count, setCount] = React.useState(0);
  const increment = React.useCallback(() => setCount((count) => count + 1), []);

  return (
    <div>
      <button onClick={increment}>Clicked {count} times</button>
    </div>
  );
}

export default MyExtension;

Just a simple counter - not more. We only use this example to demonstrate how easy it is to bring interactive components from one thing to another. This component is actually lazy loaded in its position. So it will only be made available when some slot would demand it, which is great for performance reasons.

Once published we can go back and look at the aggregation of both micro frontends in the same view:

Even without running our own shell we could debug both together. The piral-cli supports running multiple micro frontends at once. From the first pilet run:

npx pilet debug ../first-pilet ../second-pilet

where you can replace the names of the two folders with the paths you gave your pilets.

Congratulations! You just created two pilets that are capable of running in different app shells and bring features to each other without depending on each other.

Conclusion

In this article guide, we learned all about micro frontends in detail. We started by introducing you to micro frontends, how they differ from traditional SPAs and what problems they solve. Then, we stated a few of its benefits and challenges regarding how good it is with its independent iterative approach and how payloads and environment differences still pose a challenge.

Next, we compared different approaches to micro frontends briefly. We finally saw how to create your first app with Piral using the Piral Feed Service.