Tutorial: How to Implement a Micro Front-end Architecture for Your App
Micro frontends first came to light in 2016 as front-end architecture similar to and loosely based on microservices. The architecture was developed in response to common issues with building a growing single-page application, like poor scalability, maintainability and developer experience.
Since, they’ve continued to evolve, allowing teams to build robust, feature-rich applications with more flexibility, accessibility and speed. In this article, I’ll unpack the theory behind Micro Frontends using a real-world demo app. We’ll explore how to implement this architecture as well as some of the advantages and challenges it can present.
Table Of Contents
What are Microservices?
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.
Who is using Micro Frontends?
Microservices have gained considerable popularity over the years and have been adopted by companies like IKEA, HelloFresh, SoundCloud and Spotify. Since 2016, roughly 24.4% of developers have used Mirco Services. Companies like IKEA use a micro frontend to break down projects into small self-contained pieces. This allows them to keep teams smaller and more focused on a single part of an application. For Spotify, micro frontends enable easier management of multiple platforms. Combining the micro frontends of each platform application with an event bus allows for easier communication between programs and servers.
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 Front-end 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:
As you can see, the app is divided into 3 core capabilities:
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:
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:
In order for the user to have a unified and consistent experience, I integrated these micro frontends into a very thin 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 Front-ends 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 front-ends, 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 directory4*/56function getPathToEmbedAssets() {}7/**8* Exposes paths to embed assets9*/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: songsActions3}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.hasError8 ? (9 <div>10 <p className="alert alert-warning">11 Something went wrong12 </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:
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:
Thanks to micro front-ends, all the benefits that are usually found in applications built with microservices, can now be leveraged in the front-end.