Web DevelopmentMicrofrontend Architecture Guide

Explore micro frontends: their benefits, challenges, and managing complexities of micro frontend architecture.
Updated at19.04.2024
Published at07.11.2023
Marcin Sadowski
Marcin Sadowski

CTO @ JS and Web3 Expert

Table of contents

  1. Introduction
  2. What are microservices?
  3. What’s micro frontend architecture?
  4. What are the benefits of micro frontends?
  5. Pure JS example
  6. Example in Next.js
  7. Potential issues
  8. What's next?
  9. Frontend Development Insights: Mastering ReactJS and VueJS

Share the article

Introduction

In the ever-evolving landscape of web development, the concept of "microfrontends" has emerged as a beacon of modular innovation, much like its predecessor, the microservices architecture in backend development. This comprehensive guide is designed to navigate you through the intricacies of the micro frontend architecture and approach, a strategy that is revolutionizing how developers construct and manage user interfaces.

Microfrontends, or micro-frontends, bring the benefits of microservices to the front-end realm. By breaking down monolithic front-end codebases into smaller, more manageable pieces, a micro-frontend architecture enables teams to work on discrete features independently, without stepping on each other's toes. This modular structure not only enhances scalability and maintainability but also accelerates the development process, allowing for continuous deployment and improvement of individual components without disrupting the entire application.

As we delve deeper into the world of micro-frontend design, we will explore its principles, best practices, and real-world applications. Whether you're a seasoned developer or new to the concept, this guide aims to equip you with a solid understanding of microfrontends, providing you with the knowledge to implement this architectural style effectively in your projects.

Join us as we embark on a journey through the micro-frontend landscape, where we'll dissect the methodology, address common challenges, and showcase the profound impact it can have on your development workflow and end-user experience.

What are microservices?

First, we need to know what backend microservices architecture is. This is the name for the architecture approach in various applications. The main points of this structure and the benefits of microservices are:

  • independent development of each module by backend teams

  • clear division between modules

  • good scalability 

  • potential less overall costs of maintenance

Traditional microservices pattern is designed to enable rapid software development on “micro modules”. As every module can be developed and tested independently, we can organize our teams and maximize their efforts by allowing them to focus only on tasks related to their modules and proper backend communication.

As microservice idea, we can understand the following sentences. Typically, microservice is hosted within containers on cloud servers like AWS or Google Cloud. Each microservice is responsible for a specific function, such as search, payment, filtering, scanning, or converting. 

Delivering software that way can solve a lot of problems related to deployments, fixes, and tests. When our team operates on the monolith, then every small fix or change needs redeployment of the whole application. With microservices, we can simply redeploy only one module at a time, which can be faster and cheaper. This type of architecture enables you to make small updates or modifications to one function without affecting the operation of the other components. Microservices are great and experienced teams can benefit from it.

Example of microservices architecture

In the world of software architecture and modern web app, microservices have gained immense popularity for their ability to break down complex server applications into smaller, more manageable components. To better understand how this server architectural style works in practice, we'll dive into an illuminating example of microservices architecture in action. In this section, we'll explore a real-world case study of back end architecture that showcases the key principles, benefits, and challenges of implementations of server microservices.

Netflix

In the past, they have presented how they do it on Netflix. In the screenshot below you can see what was it like in early 2000:

microservices microfrontend architecture

Image 1: Netflix Speech at QCon Conference 2016 | Source: YouTube

And how it looks now based on the main layers of server architecture

microservices micro frontend architecture guide

Image 2: Netflix Speech at AWS Reinvent 2019 | Source: YouTube

And in detail:

source: youtube (https://www.youtube.com/watch?v=-mL3zT1iIKw)

Image 3: Netflix Speech at AWS Reinvent 2015 | Source: YouTube

Every service is designed so that the Netflix application runs smoothly everywhere in the world without any disruptions. As you can see there are a lot of small manageable pieces coupled into one big system. Netflix decomposes its application into numerous small, independent microservices and microapps where each is microservice backend. These microservices handle specific tasks, such as recommendations, user profiles, streaming, content delivery, and more. Each microservice is responsible for its own functionality, and they communicate with each other through well-defined APIs.

Uber

Uber, embraced a server microservices and microapps architecture in the early 2010s because, at that time, they predominantly relied on two monolithic services, which presented them with numerous operational challenges that server microservices were designed to address.

Here is the screenshot of their architecture:

source: https://www.uber.com/en-PL/blog/microservice-architecture/

Image 4: Uber Article | Source: Uber

Spotify

Also in Spotify, they need server microservice architecture patterns to make their application fully accessible and functional worldwide. Below, I’m pasting the screenshot from the conference where part of this infrastructure has been shown:

source: youtube (https://www.youtube.com/watch?v=7LGPeBgNFuU)

Image 5: Spotify conference - screenshot of slide | Source: YouTube

As you can see every server example is complex and demanding, however, with these microservice patterns and design, we can use these applications every day without any issues so definitely server microservices do their job.

What’s micro frontend architecture?

Let’s start with micro frontend now. Micro frontends represent an architectural strategy for constructing modern web application and their web components by subdividing them into smaller, more easily handled frontend components or microservices. As you can see, it’s quite the same as microservices on the backend side. Below you can see the graphic that represents this approach from a very abstractive point:

microservices microfrontend micro frontend micro-frontend  architecture guide personal concept

Image 6: Abstractive Microfrontend | Source: Personal

In the graphic, we can see 3 blocks that present some microfrontend elements. Behind them, we can put anything we want, and it might be:

  • User Interface (UI): things like small web components can be split into small chunks and developed separately. Or we might split our UI into views and compile them together afterward.

  • Business Logic: Microfrontends often encapsulate specific business logic related to their function. This can include data processing, validation, and decision-making related to the functionality they provide. Eq. we can split our microfrontends into specific modules like checkout or user settings.

  • Data Models: Microfrontends can have their own data models and storage mechanisms. They might retrieve data from APIs, databases, or other sources and manage it independently.

  • State Management: Some micro frontends handle their state management. This can include user session data, preferences, and temporary storage of information related to the microfrontend's specific function or hook.

  • Routing and Navigation: Microfrontends often have their own routing and navigation components, allowing users to navigate within the microfrontend's specific section of the application in the browser.

  • Events and Messaging: Microfrontends may use event-driven or messaging systems available in the browser to communicate with other parts of the application or to react to external events.

  • User Authentication: Some micro frontends handle user authentication and authorization, managing user logins and permissions in the browser for the features they provide. Think of it as a proxy for proper authentication.

  • Localization and Internationalization: Some micro frontends manage their own localization and internationalization features to cater to different languages and regions.

  • Error Handling and Logging: We can encapsulate this logic into micro frontends architecture as well. Handling errors specific to their functionality and logging relevant information for debugging and monitoring.

  • Third-Party Integrations: If necessary, micro frontends can incorporate third-party services or integrations, such as payment gateways, social media widgets, or analytics tools.

So as you can see there are a lot of things that can be turned into a microfrontend module. That’s why this kind of approach is getting more popular. 

Do you need support with taking the frontend architecture of your web app to a higher level?

As Mobile Reality we deeply specialize in the custom software solutions. We provide our Clients with end-to-end custom software development services. Don't hesitate to contact us.
CEO of Mobile Reality
Matt Sadowski
CEO
Or contact us:
North America
+1 718 618 4307
hello@themobilereality.com
European Union
+48 607 12 61 60
hello@themobilereality.com

Success!

Your message has been sent.

Our sales representative will contact you shortly.

How did things look like before microfrontends?

Before microfrontends so before the release of Webpack v5 in 2020 Frontend developers built a simple frontend monolith, monolithic frontends or might have the need to encapsulate some logic separately and spread this between various other applications and web components. Monolithic approach in the frontend environment was the only way. In order to make it somehow different then, for instance, the frontend world, they had to use the tools pointed out below.

NPM packages

To do it you have to start by writing the micro frontend code, utilizing it for your specific functionality in the front. Configure your front project by specifying dependencies in your package.json. Use a build tool or script to bundle and package your micro frontend code. After that, you need to define, export, and publish in NPM the modules, components, or functions from your micro frontend to be used by the parent application. When everything is published then you need to integrate with the Parent Application. Install the micro frontend as an npm package in the parent application, import the exported modules, and integrate them as needed.

ESI modules (Edge Side Includes)

In that case, you have to choose the ESI-Capable service. Select a content delivery network (CDN) or reverse proxy with ESI support, like Fastly or Cloudflare. You need to Identify distinct parts of your frontend for micro frontends, each handling specific functionality. Build micro frontends using your preferred frontend stack. After that, you have to insert ESI tags into your HTML templates to specify where micro frontends should appear. Configure your CDN to intercept ESI-tagged requests, define rules for inclusion, and set up caching strategies. In the case of SSR, you need to create a backend service to aggregate data from micro frontend APIs and dynamically assemble the final HTML response, replacing ESI tags with micro frontend content. 

Use a framework designed for it like single-spa

Begin by installing the Single Spa library in your micro front end project using npm install single-spa or yarn add single-spa. Create your micro frontend using your chosen JavaScript framework or library. Within your micro frontend code, register it with Single Spa, defining its name, mount, and unmount functions. At the end, you need to set up routing for your microfrontend within your main application, specifying when and where it should be loaded. Implement integration and communication mechanisms as needed.

Of course, you could build something from scratch and manage all micro frontends on your own. However, this approach is not accessible to all teams.

What are the benefits of micro frontends?

Ok, so if front end developers could live without it and build monolithic applications then why do we need it at all? Well, the solutions I have given above addressed some issues, however, didn’t solve them completely. Apart from the Single Spa library which has been built for this matter, it’s worth mentioning that this framework didn’t support SSR until January 2022 so usage of Single Spa was kind of limited. Now with Webpack v5, we can build our microfrontends freely with dedicated stable API. So what kind of capabilities we can achieve with it? What are the benefits of micro frontend?

  • Independent Development: Microfrontends allow different teams to work on separate components simultaneously, speeding up feature delivery and reducing frontend development process bottlenecks if you compare it to the development of monolithic application. We can make our own small deployable components.

  • Flexibility in Technology: Micro frontends facilitate autonomous teams to have the freedom to select the most suitable technology stack for their specific micro frontend, leveraging the best libraries, tools and frameworks instead of a single technology stack for their tasks. They can use same framework if they want.

  • Scalability: Microfrontends can be independently scaled. When a user demands to grow in particular areas of the application, resources can be allocated to those specific micro frontends without affecting the entire system.

  • Isolation and Fault Tolerance: Issues in one micro frontend don't necessarily disrupt the entire application. Isolation helps contain and resolve problems in specific components without causing widespread failures.

  • Reuse and Sharing: Code and components can be shared across different parts of the application between multiple teams, promoting consistency and reducing redundancy, particularly for common UI elements like buttons and headers.

  • Improved Testing: Smaller, focused micro frontends are easier to test during runtime, leading to higher code quality and a more stable application.

  • Rapid Deployment: Updates to one micro frontend can be deployed without impacting the entire application, reducing deployment-related risks and enabling more frequent updates.

  • Enhanced User Experience: Microfrontends can enhance the user experience by allowing each team to specialize in optimizing their specific areas, resulting in more efficient and tailored user interfaces.

  • Team Autonomy: Various teams or departments can work on their micro frontends independently, fostering a sense of ownership and autonomy.

  • Simplified Maintenance: Smaller codebases are easier to maintain, simplifying issue troubleshooting and the application of updates.

  • Gradual Transition: Microfrontends facilitate a gradual shift from a monolithic frontend to a microservices-based architecture. Organizations can start by creating micro frontends for new features and gradually refactoring existing components.

  • Unified Ecosystem: Microfrontends contribute to a more organized and coherent application ecosystem, particularly beneficial for large and complex applications.

  • Reduced Build Times: Smaller codebases typically result in quicker build times, which can enhance developer productivity.

As we see, there are numerous benefits and advantages of microfrontend architecture. Almost every frontend team can change their way of working with micro frontends.

Pure JS example

Ok! So let’s build a very simple and straightforward example of a microfrontend written only with JS and HTML. We will start with the project structure.

Create a directory structure for your micro frontend application:

pure-js-microfrontend/
├── index.html
├── main.js
├── app1/
│   ├── index.html
│   ├── app.js
├── app2/
│   ├── index.html
│   ├── app.js

Here is our index.html

<!DOCTYPE html>
<html>
<head>
     <title>Microfrontend Example Pure JS</title>
</head>
<body>
    <div id="app1"></div>
    <div id="app2"></div>
    <script src="main.js"></script>
</body>
</html>

Our main js file will look like this:

async function loadService(name, id) {
    const response = await fetch(`/${name}/app.js`);
    const code = await response.text();
    const script = document.createElement('script');
    script.textContent = code;
    document.getElementById(id).appendChild(script);
}

loadService('app2', 'app2');
loadService('app1', 'app1');

We set this one simple function which will be responsible for fetching these microfrontends dynamically. 

Our app.js file from the app1 directory looks like this:

const message1 = '[Here is app 1] Successfully loaded';
console.log(message1)
window.parent.postMessage('This is message from app1 received by app2' , '*');

And app.js file from app2

const message2 = '[Here is app 2] Successfully loaded';
console.log(message2)

window.addEventListener('message', (event) => {
    if (event.data && typeof event.data === 'string') {
        console.log(`Received Message: ${event.data}`);
    }
});

And in these files, we have simple communication between microfrontend app1 and microfrontend app2. App1 is sending a message to the global scope and App2 is responsible for receiving it. In the end, we have a working example:

source: personal localhost

Image 7: Pure JS Example - 1st screenshot | Source: Personal

Of course that’s simple text but we can send anything we want, even functions. Let me show you. Let’s modify our html, app.js and main.js files.

First, index.html in root directory.

<!DOCTYPE html>
<html>
<head>
    <title>Microfrontend Example Pure JS</title>
</head>
<body>
    <div id="app1"></div>
    <div id="app2"></div>
    <div id=”menu”></div>
    <script src="main.js"></script>
</body>
</html>

After that our app.js in the app2 directory:

const message2 = '[Here is app 2] Successfully loaded';
console.log(message2)

window.addEventListener('message', (event) => {
    if (event.data && typeof event.data === 'string') {
        console.log(`Received Message: ${event.data}`);
    }
});

// Declare function
function greet(name) {
    console.log(`Hello, ${name}!`);
}

// Send the function to main.js
window.parent.postMessage({ type: 'function', name: 'greet', code: greet.toString() }, '*');

And finally our main.js file:

async function loadService(name, id) {
    const response = await fetch(`/${name}/app.js`);
    const code = await response.text();
    const script = document.createElement('script');
    script.textContent = code;
    document.getElementById(id).appendChild(script);
}

loadService('app2', 'app2');
loadService('app1', 'app1');

let functionFromApp2;

window.addEventListener('message', (event) => {
    if (event.data && event.data.type === 'function') {
        // Parse the function and execute it
        const { code } = event.data;
        const receivedFunction = new Function(`return ${code}`)();
        const node = document.createElement("button");
        node.setAttribute("id", "mainButton");
        const textnode = document.createTextNode("Check me");
        node.appendChild(textnode);
        document.getElementById('menu').appendChild(node);
        
        functionFromApp2 = receivedFunction;
    }
});

document.addEventListener('click', (event) => {
    if (event.target.id === 'mainButton') {
        functionFromApp2('Thats message from app2');
    }
});

The results are the following:

source: personal localhost

Image 8: Pure JS Example page - 2nd screenshot | Source: Personal

As you can see. We can use our greet function from app2 simply in our main.js file. That’s a very basic example, and we definitely can’t call it a complete solution for microfrontends applications. If we want to build our application that way, then apart from what we have right now, we set a lot of things. For instance, proper scoped communication between modules. That’s why the community has built some frameworks already.

Available tools

Here is the list of available different frameworks or libraries to build stable and maintainable micro frontend applications:

Single Spa

source: https://single-spa.js.org/ microservices microfrontend micro frontend micro-frontend  architecture guide

Image 9: Single SPA Logo | Source: Single SPA

Single Spa is a popular open-source JavaScript framework that is specifically designed for building micro frontend applications. It enables developers to create modular, maintainable, and scalable web applications by allowing the integration of multiple frontend frameworks or libraries into a single application. Also, it set principles for building microfrontends. Single Spa offers a flexible and framework-agnostic approach to micro frontend architecture. Single Spa has gained popularity in the web development community as a valuable solution for implementing micro frontend architectures. It allows organizations to achieve flexibility, modularity, and independence in their frontend development, making it easier to manage complex applications and transition between different technologies.

Luigi

source: https://luigi-project.io/  microservices microfrontend micro frontend micro-frontend  architecture guide

Image 10: Luigi Logo | Source: Luigi

Luigi is an open-source micro frontend framework, created by SAP Customer Experience Labs, which streamlines the development of intricate applications by breaking them down into self-contained micro frontends. This framework has garnered recognition, primarily in large enterprise settings, due to its focus on modularization and maintainability. It is especially advantageous for intricate web applications that require division into smaller, reusable components, simplifying the processes of development, testing, and deployment.

Piral

source: https://piral.io/  microservices microfrontend micro frontend micro-frontend  architecture guide

Image 11: Piral Logo | Source: Piral

Piral is a framework for building micro frontends and managing them within a unified application. It offers a set of tools and conventions that simplify the development and integration of micro frontends, making it easier to create modular and extensible web applications. Piral is a valuable tool for organizations looking to adopt a micro frontend architecture to create maintainable, scalable, and extensible web applications. It offers a structured approach to micro frontend development while remaining adaptable to different technology stacks and project requirements.

Podium

source: https://podium-lib.io/  microservices microfrontend micro frontend micro-frontend  architecture guide

Image 12: Podium Logo | Source: Podium

Podium, an open-source JavaScript library, simplifies the creation of micro frontend applications by enabling the development of independent and scalable micro frontends that can be seamlessly combined to construct a unified web application. This framework is particularly well-suited for projects that require a micro frontend architecture, allowing the composition of complex web applications from self-contained components. It equips developers with essential tools and best practices to ensure a cohesive user experience and to streamline the development and maintenance of micro frontends.

Qiankun

source: https://qiankun.umijs.org/  microservices microfrontend micro frontend micro-frontend  architecture guide

Image 13: Qiankum Logo | Source: Qiankum

Qiankun, developed by Ant Financial Services Group (now part of Alibaba Group), is an open-source JavaScript library and microfrontend framework. Its primary purpose is to simplify the creation and administration of micro frontends within a single, cohesive application. Qiankun is a valuable asset for organizations seeking to implement a microfrontend architecture, offering streamlined development and integration of microfrontends while remaining adaptable to a variety of technology stacks and project needs.

Mosaic

source: https://docs.mosaic.js.org/  microservices microfrontend micro frontend micro-frontend  architecture guide

Image 14: Mosaic Logo | Source: Mosaic

Mosaic it’s very small but a modern, open-source microfrontend library for building modular web applications. It provides a structure by setting plugins for building micro frontends that can be loaded at runtime. It is designed to be used with any JavaScript framework, such as React, Vue, or Angular. Overall, MosaicJS is a powerful and flexible JavaScript framework that can be used to build extensible, modular, and reusable applications.

Example in Next.js

Let’s do something that can be production-ready what we can put into "modern web applications" term. We will do a simple micro frontend react next js application with an explanation what is needed to develop it in the long run. To make it we will use Webpack v5 plugin from Module Federation. It will allow us to stay in the Next.js environment and apply micro frontends as easy as possible. I will follow this official example from their GitHub repository - https://github.com/module-federation/module-federation-examples/tree/master/nextjs-v13. Let’s begin with the project structure. It will look like this:

├ next-js-microfrontends/
├── package.json
├── my-components/
│   ├── package.json
│   ├── next.config.js
│   ├── components/
│         ├── button/
│              ├── index.tsx
│   ├── public/
│   ├── src/
│          ├── pages/
│              ├── index.tsx
│          ├── styles/
│
├── checkout/
│   ├── package.json
│   ├── next.config.js
│   ├── pages-map.js
│   ├── public/
│   ├── src/
│          ├── pages/
│              ├── product/
│                 ├── index.tsx
│          ├── styles/
│ 
├── shop/
│   ├── package.json
│   ├── next.config.js
│   ├── public/
│   ├── src/
│          ├── pages/
│              ├── [...slug].tsx
│          ├── styles/
├── shared/
│   ├── index.d.ts
│   ├── index.js
│   ├── package.json

As you can see, there are some files needed for this, however, we need these applications I can explain some things there. Let’s begin with our package.json file in the root project which contains this code:

{
  "name": "next-js-microfrontends",
  "private": true,
  "workspaces": [
    "./my-components",
    "./shop",
    "./checkout",
    "./shared"
  ],
  "version": "1.0.0",
  "scripts": {
    "start": "concurrently \"npm --prefix my-components run dev\" \"npm --prefix shop run dev\" \"npm --prefix checkout run dev\"",
    "build": "concurrently \"npm --prefix my-components run build\" \"npm --prefix shop run build\" \"npm --prefix checkout run build\"",
    "serve": "concurrently \"npm --prefix my-components run start\" \"npm --prefix shop run start\" \"npm --prefix shop run start\""
  },
  "dependencies": {
    "concurrently": "^8.2.1"
  }
}

It’s a simple setup for running our applications concurrently. We need to run all microfrontends applications which are needed for our main application.

Now let’s go to the my-components/package.json file:

{
  "name": "my-components",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev -p 3001",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@module-federation/nextjs-mf": "7.0.8",
    "next": "13.5.4",
    "react": "^18",
    "react-dom": "^18",
    "shared-stuff": "file:../shared"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "13.5.4",
    "typescript": "^5"
  }
}

Every package.json looks nearly the same between shop and checkout apps so I won’t paste their package.json files as it would extend this article with no sense. In package.json we mainly have base next.js dependencies. Apart from that, we have also our Webpack plugin installed as dependency @module-federation/nextjs-mf. This plugin needs to be installed in every microfrontend app. We will you it in our next.config.js file which looks like this for my-components application:

/** @type {import('next').NextConfig} */
const NextFederationPlugin = require('@module-federation/nextjs-mf');

const nextConfig = {
  reactStrictMode: true,
  webpack(config, options) {
    config.plugins.push(
      new NextFederationPlugin({
          name: 'my-components',
          remotes: {},
          filename: 'static/chunks/remoteEntry.js',
          exposes: {
              './button': './components/button/index.tsx',
          },
          shared: {},
          extraOptions: {}
      }),
    );

    return config;
  },
}

module.exports = nextConfig

The most vital parameters in this object are exposes and remotes. Exposes is responsible for pointing files that we would like to expose to an external module. Remotes tell what module we would like to consume. In microfrontend my-components we would like to expose only our general components to other modules. In this case, it’s only a button. Let’s check what our button looks like:

import React, { useEffect } from 'react';

type TButton = {
    title: string;
    type: string;
}

const Button = (props: TButton) => {
    console.log('---------loading remote component---------');
    const { title, type } = props;
    useEffect(() => {
        console.log('On mount', type);
    }, []);

    return (
        <button>
            {title}
        </button>
    );
};

export default Button;

As you can see, this component is exactly the same as a component which we would like to have in the local project. There is no difference. 

Now we would like to consume exposed components. In this case, we need to check our next.config.js file in the checkout microfrontend application:

/** @type {import('next').NextConfig} */
const NextFederationPlugin = require('@module-federation/nextjs-mf');

const remotes = () => {
  return {
    button: `my-components@http://localhost:3001/_next/static/chunks/remoteEntry.js`,
  };
};

const nextConfig = {
  webpack(config, options) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'checkout',
        remotes: remotes(),
        filename: 'static/chunks/remoteEntry.js',
        exposes: {
              './product': './src/pages/product/index.tsx',
              './pages-map': './pages-map.js',
        },
        shared: {},
        extraOptions: {
              exposePages: true,
        }
      }),
    );
    
    return config;
  },
}

module.exports = nextConfig

As you can see, we point to this file by putting this line:

button: `my-components@http://localhost:3001/_next/static/chunks/remoteEntry.js`,

In this line, we set how we would like to consume this microfrontend app. In this case, we call it just a button. After that, we point to the name my-components which we have set in the next.config.js file in my-components directory. Then, we point to the correct running server. For my-components it’s locahost:3001. 

Let’s check how we can use this button component in our checkout application. We will use it under the product page so in pages/product/index.tsx.

import Head from 'next/head'
import { Inter } from 'next/font/google'
import styles from '@/styles/Home.module.css'
import { lazy } from 'react';

const RemoteButton = lazy(() => import('button/button'));const inter = Inter({ subsets: ['latin'] })
export default function Home() {
    return (
        <>
            <Head>
                <title>Checkout Page</title>
                <meta name="description" content="Generated by create next app" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <link rel="icon" href="/favicon.ico" />
            </Head>
            <main className={`${styles.main} ${inter.className}`}>
                <div className={styles.description}>
                    CHECKOUT PAGE
                    <RemoteButton title="Title for my button"/>
                </div>
            </main>
        </>
    )
}

Using our button needs lazy from react to make it work. Next.js comes with its own solution for fetching chunks of webpack builds under nextjs/dynamic however it’s recommended to use React.lazy for that. I guess the reason for that is Next.js does more things under this function so it can cause the issue between Webpack and next.js environment. In the end, there is no more difference between normal local development. With that, we can see a button under our product page!

source: personal localhost

Image 15: Checkout page - screenshot | Source: Personal

Also, in the console, you can see that mounting hooks work as well!

Ok, let’s go further. In the checkout application, we have exposed two files: product page (this one, which you can see in the screenshot) and pages-map.js. We expose it because microfrontend, as mentioned at the beginning, can be used for different things, not only for exposing small components. We already know what the product page looks like. Let’s now see our pages-map.js file under the checkout application:

export default {
    '/product': './product'
};

And that’s it. Nothing more or less. This file is consumed by our third app, which is shop. Let’s go there. Firstly, let’s check the next.config.js file:

/** @type {import('next').NextConfig} */
const NextFederationPlugin = require('@module-federation/nextjs-mf');

const remotes = () => {
    return {
        checkout: `checkout@http://localhost:3002/_next/static/chunks/remoteEntry.js`,
    };
};

const nextConfig = {
  webpack(config, options) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'shop',
        remotes: remotes(),
        filename: 'static/chunks/remoteEntry.js',
        exposes: {},
        shared: {},
        extraOptions: {
        }
      }),
    );
    return config;
  },
}

module.exports = nextConfig

Here, you can see that our remotes is only checkout application. Where is our consumed pages-map? And why do we need something like that? To answer this question, we need to look at this from the development perspective.

Imagine that you develop such a microfrontend application. You work on your module, component, or other functionality. You know that your work will be a part of something bigger. It would be nice to have a full picture of the whole application that you are building. There is an answer to our question. Pages-map.js lets us do such a thing. In our application, we can consume this file and run, in this case, pages from other applications and we do not have to open other localhosts in our browsers to check all routes in the application. That small thing resolves fundamental problems when working together and you want to know how things look like in other modules. Also, this should be used for tracing bugs and reporting. With something similar like pages-map.js we can expose other services and create a proper crash and issue center with all needed information. Ok! But how the pages-map.js is consumed if I can’t see it under next.config.js file? Everything is handled under generic route handler [...slug].js in the pages directory. It looks like this:

import { createFederatedCatchAll } from 'shared-stuff';

export default createFederatedCatchAll([]);

Here, you can see that we have reached our fourth module, which is shared-stuff. The shared module includes mainly one file index.js, which is the full copy of what you can find in the official module federation repository (https://github.com/module-federation/module-federation-examples/blob/master/nextjs-v13/shared/index.js). The full file you can find there. Here, I will paste the createFederatedCatchAll and machFederatedPage functions:

async function matchFederatedPage(path) {
    const maps = await Promise.all(
        Object.keys(remotes).map(async remote => {
            const foundContainer = injectScript(remote);
            const container = await foundContainer;
            return container
                .get('./pages-map')
                .then(factory => ({ remote, config: factory().default }))
                .catch(() => null);
        }),
    );

    const config = {};
    for (const map of maps) {
        if (!map) continue;
        for (let [path, mod] of Object.entries(map.config)) {
            config[path] = {
                remote: map.remote,
                module: mod,
            };
        }
    }

    const matcher = createMatcher.default(config);
    return matcher(path);
};

createFederatedCatchAll() {
        const FederatedCatchAll = initialProps => {
            const [lazyProps, setProps] = React.useState({});
            const { FederatedPage, render404, renderError, needsReload, ...props } = {
                ...lazyProps,
                ...initialProps,
            };
            React.useEffect(() => {
                if (needsReload) {
                    const runUnderlayingGIP = async () => {
                        const federatedProps = await FederatedCatchAll.getInitialProps(props);
                        setProps(federatedProps);
                    };
                    runUnderlayingGIP();
                }
            }, []);
            
            if (render404) {
                // TODO: Render 404 page
                return React.createElement('h1', {}, '404 Not Found');
            }
            if (renderError) {
                // TODO: Render error page
                return React.createElement('h1', {}, 'Oops, something went wrong.');
            }

            if (FederatedPage) {
                return React.createElement(FederatedPage, props);
            }

            return null;
        };

        FederatedCatchAll.getInitialProps = async ctx => {
            // Bot marks "req, res, AppTree" as unused but those are vital to not get circular-dependency error
            const { err, req, res, AppTree, ...props } = ctx;
            if (err) {
                // TODO: Run getInitialProps for error page
                return { renderError: true, ...props };
            }
            if (!process.browser) {
                return { needsReload: true, ...props };
            }

            const matchedPage = await matchFederatedPage(ctx.asPath);

            try {
                const remote = matchedPage?.value?.remote;
                const mod = matchedPage?.value?.module;

                if (!remote || !mod) {
                    // TODO: Run getInitialProps for 404 page
                    return { render404: true, ...props };
                }

                console.log('loading exposed module', mod, 'from remote', remote);
                const container = await injectScript(remote);
                const FederatedPage = await container.get(mod).then(factory => factory().default);
                console.log('FederatedPage', FederatedPage);
                if (!FederatedPage) {
                    // TODO: Run getInitialProps for 404 page
                    return { render404: true, ...props };
                }

                const modifiedContext = {
                    ...ctx,
                    query: matchedPage.params,
                };
                const federatedPageProps = (await FederatedPage.getInitialProps?.(modifiedContext)) || {};
                return { ...federatedPageProps, FederatedPage };
            } catch (err) {
                console.log('err', err);
                // TODO: Run getInitialProps for error page
                return { renderError: true, ...props };
            }
        };

        return FederatedCatchAll;
    }

This function does a simple exploration of what’s exposed under a specific module in pages-map. In our case createFederatedCatchAll do this for the checkout microfrontend app, but we could have more apps that every expose pages-map.js file. Then, these modules would also be explored if our app has this application in remotes object. Thanks to that, if we go to the product page under our shop app, we should see this particular page instead of 404!

source: personal localhost

Image 16: Shop page - screenshot | Source: Personal

More information about pages map you will be able to find from this great conversation in the YouTube video: https://www.youtube.com/watch?v=m-eBqbFFUXg 

Great! We have reached our example microfrontend application in Next.js. As I mentioned at the beginning, everything is based on the official Module Federation example here https://github.com/module-federation/module-federation-examples/tree/master/nextjs-v13 . Go and check out other examples!

Potential issues

We have pointed out all positive things related to microfrontends work. Let’s focus now on some disadvantages and challenges of microfrontend design. Here is some general:

  1. Complexity: With the three applications above, we can see clearly that managing a larger number of smaller, interconnected components can increase the complexity of the application on the client or server side. Eventually, it can lead to challenges in understanding the overall architecture.

  2. Consistency: Like pages-map files, we need to be consistent in other files. It can be a trouble while working on a large project with different services. Consistent user experience across microfrontends is also vital. Things like unified look and feel and navigation can be challenging and may require additional effort.

  3. Communication: Coordinating communication is always hard. Data sharing between microfrontends has to be organized and can be complex, especially when dealing with cross-cutting concerns like authentication, authorization, or state management.

  4. Dependency Management: Depends on tools you choose to build your microfrontend application but handling dependencies between microfrontends and sharing common libraries can lead to complexity and potential conflicts. With Webpack v5 it has been handled in most cases. However, sometimes you can experience edge cases while working with some libraries.

  5. Testing: Testing microfrontends can be more complex. Isolation helps with debugging but can lead to issues when the developed component is getting more involved in the project. If we add to it different versions of its, then it can be hard to manage. 

  6. Routing and Navigation: Coordinating routing and navigation between microfrontends, especially in NextJS where everything relies on the pages directory can be an issue.

  7. Debugging: Debugging issues across multiple microfrontends can be challenging. Just imagine how things should look like when we would like to set up a Sentry error handler for the whole application. 

  8. Deployment: It can be a trouble when we develop multiple microfrontends. Especially when they are developed and managed by different teams. Also, we need to remember that we do not want to resign from the cache on the client side so our newly deployed chunks can also omitted in these cases. Development and deployment can be an issue in that case.

  9. Team Collaboration: It’s always hard. Teams working on different microfrontends may have different development processes, technologies, and standards, affecting collaboration and project cohesion.

  10. Knowledge Transfer: Onboarding new teammates can be challenging in these cases, apart from being familiar with tools, you have to get familiar with the whole communication between other teams. Also, handing off a microfrontend from one team to another can be an issue when teams have different expertise and domain knowledge.

Besides the most general list, we have also issues related to tools that give us the possibility to start microfrontend applications quickly. For instance, in the front end application, the plugin for Next.js from Module Federation is working only with Next.js with pages setup. It does not work with app routing yet. What’s more, in the example above, if you try to use the next/router in any module, then you will encounter an issue related to the shared module scope. More information about it here: https://github.com/module-federation/universe/issues/1104 . And finally, these tools are under active development. You and your teams should keep in mind that.

What's next?

Microfrontends are rapidly changing the landscape of web development. This article delves into the concept of microfrontends, which involves breaking down web applications into smaller, autonomous components and highlights their current benefits and challenges.

Looking ahead, the future of microfrontends appears promising. As this architectural approach continues to gain traction, we can anticipate further advancements in tooling and best practices to simplify the development and management of microapps. Moreover, collaboration among teams and frameworks to address challenges like performance and security is expected to grow.

In the coming years, microfrontends may become a standard practice for web development, offering more agility, scalability, and flexibility to organizations. Developers should keep an eye on this evolving space as it transforms the way we build, maintain web applications, and create resilient web design.

Frontend Development Insights: Mastering ReactJS and VueJS

Are you fascinated by the evolving landscape of frontend development, particularly in ReactJS and VueJS? At Mobile Reality, we are eager to share our rich expertise and insights in these cutting-edge technologies. Uncover the challenges we navigate, the innovative strategies we employ, and the groundbreaking solutions we deliver in ReactJS and VueJS development. Immerse yourself in our curated selection of articles, each a deep dive into aspects of these powerful frontend frameworks:

Delve into these comprehensive resources to enhance your understanding of ReactJS and VueJS. For any inquiries or if you’re considering a career with us, don't hesitate to contact us or visit our careers page to submit your CV. Join us in shaping the future of front-end development!

Did you like the article?Find out how we can help you.

Matt Sadowski

CEO of Mobile Reality

CEO of Mobile Reality

Related articles

Discover essential React JS Best Practices for Frontend Development Teams. Elevate your coding game with expert tips and techniques.

26.04.2024

Top ReactJS Best Practices for Frontend Teams

Discover essential React JS Best Practices for Frontend Development Teams. Elevate your coding game with expert tips and techniques.

Read full article

Discover the essential guide for CTOs comparing GO vs Node JS. Make the right choice for your tech stack. Get insights now! #node #nodejs #go #golang #CTO

26.04.2024

GO vs Node JS : A Complete Comparison for CTOs

Discover the essential guide for CTOs comparing GO vs Node JS. Make the right choice for your tech stack. Get insights now! #node #nodejs #go #golang #CTO

Read full article

Discover the essential guide for CTOs comparing Node JS vs PHP. Make the right choice for your tech stack. Get insights now! #nodejs #php #CTO

26.04.2024

Node JS vs PHP: A Complete Comparison for CTOs

Discover the essential guide for CTOs comparing Node JS vs PHP. Make the right choice for your tech stack. Get insights now! #nodejs #php #CTO

Read full article