Tutorial: How to Implement a Micro Front-End Architecture for Your App

ProfilePicture of Leo Cristofani
Leo Cristofani
Senior Developer
A desk with a laptop computer and frontend technologies logos

As business logic increasingly moves to the front-end, more and more technologies are moving with the trend. In this article, we’ll explore how microservice architecture – typically associated with back-end development – is now available for front-end developers. I’ll explain the theory of micro frontends through a real-world example. This way, I can easily flag the advantages and common issues faced by you guys: the early adopters.

What Is Microservice Architecture?

Microservice architecture reduces complex applications into single-purpose components. This is a more efficient approach because components are fault isolated: if one breaks, the rest of the app continues to work.

Each component can be built by a small, cross-functional team, enabling that team to choose the ideal technology for the job while also deploying independently, at their own pace.

How Do Microservices Apply to a Micro Frontend Architecture?

I’ll attempt to answer this question within the context of an app I built using the micro frontend architecture. MiSongs, the app in question, lets users browse through different artists and musical genres before adding their favorite songs to a playlist. Hopefully, MiSongs will let you understand the concepts of micro frontends, learn the inner workings of this architecture and also serve as a quick reference for your next project.

Let’s start with a look at the MiSongs wireframe:

Screenshot of wireframe for an app example using micro frontend architecture

As you can see, the app is divided into 3 core capabilities:

  1. List artists by genre
  2. List songs by artist
  3. Add songs to a playlist.

I could have opted to build this app as a feature-rich single page app in the front-end, using a set of microservices in the back-end. In which case it would have looked something like this:

App structure of micro services example

The problem with this approach is that, as the application grows, the front-end will get overly complex and become a ‘front-end monolith’. This is something you want to avoid. Full stop. I opted to build with micro frontends and structure the app vertically, around its capabilities. With this approach, each capability is ‘owned’ by an independent team. This team is responsible not only for the database layer and back-end but also for the front-end.

The new diagram looks like this:

Vertical app structure using micro frontend

In order for the user to have a unified and consistent experience, I integrated these micro frontends into a very thin layer:

Example of micro frontends integrated into a layer

In essence, micro frontends are a way to more efficiently organize an application. By extending the concept of microservices to the front-end, teams can work independently to deliver new functionality to end users.

How to Implement Micro Frontends Step by Step

Even though MiSongs is a very simple app, I’ve chosen to break it up into 4 independent applications to show you a few different approaches to implementing micro frontends.

If you read through the code for the MiSongs app, you’ll notice that it’s composed of 4 applications:

Artists: Lists artists by genre.
(React SPA front-end + Express.js back-end)

Songs: Lists songs by artist.
(React SPA front-ends + Express.js backend)

Playlist: Adds songs to a playlist.
(React SPA front-end + Express.js back-end)

Parent: Integrates Artists, Songs, and Playlist so that they can be used as a single application.
(React SPA front-end)

From now on, when I write Parent, I’m referring to the application that integrates all micro frontends, as mentioned above.

The main challenges with micro frontends applications are usually integration, communication between services, error handling, authentication, and styles. So we’re going to address how each of these was handled in MiSongs.

Integration

In MiSongs, I opted for integration to happen at runtime in 3 distinct steps:

1. Webpack in each micro frontend generates index.[hash].js umd bundle and index.[hash].css. The [hash] in the asset name is necessary to avoid browser cache.

1module.exports = {
2 mode: 'production',
3 entry: `../client/src/embed.js`,
4 output: {
5 library: 'Artists',
6 libraryTarget: 'umd',
7 filename: 'index.[hash].js',
8 path: path.resolve(__dirname, '../server/public/embed')
9 },
10 // ...
11}
12

2. The micro frontend backend exposes an endpoint from which Parent (the integration layer) fetches the paths to index.[hash].js and index.[hash].css. These are the only assets necessary for the micro frontends to run within the Parent.

1/*
2* Generates { js: 'path/to/index.[hash].js', css: 'path/to/index.[hash].css' }
3* based on files available in /public/embed directory
4*/
5
6function getPathToEmbedAssets() {}
7/**
8* Exposes paths to embed assets
9*/
10app.get('/api/embed-assets', (req, res) => {
11  res.json(getPathToEmbedAssets());
12});
13

3. Parent calls the micro frontends’ endpoint /embed-assets to fetch the embed assets and inject them into the page:

1export function loadScript(url, name) {
2  let promise;
3  if (_scriptCache.has(url)) {
4    promise = _scriptCache.get(url);
5  } else {
6    promise = new Promise((resolve, reject) => {
7      let script = document.createElement('script');
8      script.onerror = event => reject(new Error(`Failed to load '${url}'`));
9      script.onload = resolve;
10      script.async = true;
11      script.src = url;
12      if (document.currentScript) {
13        document.currentScript.parentNode.insertBefore(script, document.currentScript);
14      } else {
15        (document.head || document.getElementsByTagName('head')[0]).appendChild(script);
16      }
17    });
18    _scriptCache.set(url, promise);
19  }
20  return promise.then(() => {
21    if (global[name]) {
22      return global[name];
23    } else {
24      throw new Error(`"${name}" was not created by "${url}"`);
25    }
26  });
27}
28export function loadStyle(url) {
29  new Promise((resolve, reject) => {
30    let link = document.createElement('link');
31    link.onerror = event => reject(new Error(`Failed to load '${url}'`));
32    link.onload = resolve;
33    link.async = true;
34    link.href = url;
35    link.rel = 'stylesheet';
36    (document.head || document.getElementsByTagName('head')[0]).appendChild(link)
37  });
38}
39

I’ve used runtime in the front-end. But there are other integration options as well:

Backend integration at runtime with server-side includes (SSIs):
Server Side Includes are a great option if the content needs to be rendered on the server. SSIs are widely supported and relatively easy to configure. The only downside to using them is that, if one of the micro frontends is slow, then the whole experience will be degraded.

Front-end integration at build time with NPM:

Instead of exposing an endpoint from which Parent can fetch the path to the required assets (like we did in MiSongs), we can export the necessary assets of each micro frontend as an NPM module. The downside to this approach is that we have to redeploy Parent every-time there’s a change to any micro frontend.

Communication Between Services

MiSongs uses Custom DOM Events to communicate between services. For example, when an artist is selected, the Artists micro frontend dispatches a custom event:

1// ...
2class ArtistsListItem extends React.Component {
3  onClick = (e) => {
4    //...
5    window.dispatchEvent(
6      new CustomEvent(ARTISTS_SELECT_ARTIST, { detail: { artist: name } })
7    );
8  }
9  render() {
10    // ...
11    return (
12      <button onClick={this.onClick}>{name}</button>
13    );
14  }
15}
16// ...
17

Then, the Songs micro frontend listens to it and responds accordingly: displaying the songs of the selected artist:

1class SongsContainer extends React.Component {
2  componentDidMount() {
3    window.addEventListener(ARTISTS_SELECT_ARTIST, this.fetchSongs);
4    this.fetchSongs({ detail: { artist: this.state.artist } });
5  }
6  componentWillUnmount() {
7    window.removeEventListener(ARTISTS_SELECT_ARTIST, this.fetchSongs);
8  }
9}
10

I like to use custom DOM events because they facilitate communication and still allow the different components to be decoupled from each other. In this case, I used custom DOM events to exchange information amongst micro frontends. Depending on your use-case, one of the following options may be more suitable:

React component props:
Parent can pass the state of a micro frontend around to other micro frontends using react component props.

Redux store:
Each micro frontend UMD can expose a reducer for example that can be integrated into Parent’s shared  redux store as such:

1{
2Component: Songs, reducer: songsReducer, actions: songsActions
3}
4

Backend:
Micro frontends can also communicate via the backend. If you opt for this method, it’s highly recommended that you don’t share the same endpoints meant for the micro frontends front-end. This will avoid dependency/coupling issues. For example, if the Songs app needs to talk to the Artists app backend, it will do so via endpoints dedicated to other micro frontends, not the endpoints used by the front-end of Songs.

Error Handling

Each micro frontends is responsible for handling its own errors. The aim is to prevent errors from one layer affecting other integrated micro frontends. In MiSongs, I leveraged the React 16 error boundary feature to do this.

Each micro frontend should be wrapped with an error boundary:

1class SongsContainer extends Component {
2  render() {
3    // ...
4    return (
5      <ErrorBoundary>
6        <Songs artist={artist} songs={songs} />
7      </ErrorBoundary>
8    );
9  }
10}
11

You can think of error boundary as a React component that implements the componentDidCatch lifecycle method:

error_boundary.js

1class ErrorBoundary extends Component {
2 //...
3 componentDidCatch(error, info) {
4 this.setState({ hasError: true });
5 }
6 render() {
7 return this.state.hasError
8 ? (
9 <div>
10 <p className="alert alert-warning">
11 Something went wrong
12 </p>
13 </div>
14 ) : this.props.children;
15 }
16}
17

Authentication

For the sake of brevity, I did not implement authentication in MiSongs. By looking at the code below, you can see that the intent is to use JWT. On lines 15, 20 and 23 Parent passes as React component props an authentication token so each micro frontend can communicate with their respective backend.

1import React from 'react';
2import MicroFrontends from './microfrontends';
3import './app.css';
4export default function App() {
5 return (
6 <div className="app">
7 <header className="app__header">
8 <h1 className="app__logo">Mi<strong>Songs</strong></h1>
9 </header>
10 <div className="app__main">
11 <div className="app__main__column">
12 <div className="microfrontend__wrapper">
13 <MicroFrontends.Artists authToken="" basename="" />
14 </div>
15 </div>
16 <div className="app__main__column">
17 <div className="microfrontend__wrapper">
18 <MicroFrontends.Playlist authToken="" basename="" />
19 </div>
20 <div className="microfrontend__wrapper">
21 <MicroFrontends.Songs authToken="" basename="" />
22 </div>
23 </div>
24 </div>
25 </div>
26 );
27}
28

To make this work: Parent calls an authentication service, gets a JSON web token and shares this token with the integrated micro frontends. That way, they can communicate with their respective backend securely. MiSongs & Authentication would look like this:

App and authentication service integrated with micro frontends

Styles

The CSS of each micro frontend is fetched from their respective /embed-assets endpoint and then injected Parent.

The downside to this approach is that of potential conflicts. That can be avoided by namespacing CSS selectors or using web components with shadow dom.

1<link href="http://localhost:3001/public/embed/index.f175bcb....css" rel="stylesheet" />
2<link href="http://localhost:3003/public/embed/index.3775bcg....css" rel="stylesheet" />
3<link href="http://localhost:3005/public/embed/index.5575bcf....css" rel="stylesheet" />
4

A Summary of Micro Frontend Benefits

I hope this article got you excited about the future of this architecture. Micro frontends might initially appear to be just another way to structure an application, but it’s one that I believe has countless benefits – especially for large teams. These include, but are certainly not limited to:

Rapid development:
Teams can develop and deploy features independently.

High flexibility:
Developers can move at their own pace and use the most appropriate technology for the job.

Better organization:
Applications made of components focused on business capabilities.

Fault isolation:
If a micro frontend breaks, the rest of the application still works

Thanks to micro frontends, all the benefits that are usually found in applications built with microservices, can now be leveraged in the front-end.


Key Takeaways

What are micro frontends?

A micro frontend is a web development pattern where frontend UI is composed of mostly independent fragments. Each of these fragments can be developed by a different team using different technologies. Micro frontends resemble the backend concept of microservices, providing similar benefits in ease of maintenance and development.

When should you use a micro frontend?

Micro frontends are best suited for situations where an application is complex. Using a micro frontend provides you with more utility for customizing a frontend to meet a specific need. In addition, each frontend component can be worked on independently, making it easier to divide each component among a team.

What are examples of microservices?

Microservices are small pieces of code that achieve a single goal. As an example, consider an application that takes orders from customers to ship products to them. There would typically be an ordering microservice that handles customer requests for orders. This microservice has both a frontend for the user to interact with and a backend to handle user requests.

Looking to hire?

Join our newsletter

Join thousands of subscribers already getting our original articles about software design and development. You will not receive any spam. just great content once a month.

 

Read Next

Browse Our Blog