Module Federation in Next.js (Microfrontend )

Module Federation in Next.js (Microfrontend )

Server-side rendered micro-frontends solution

Microfrontends in a NextJS app

Micro frontends have actually been around for a while now and have been implemented in various ways. One of the newer and exciting additions to the ecosystem is Webpack 5 and its module federation feature.

Micro frontends are an architectural pattern that has become a hot topic in recent years for many good reasons. It has helped organisations break complex UI applications into smaller, more manageable pieces, as well as enabling teams to work end-to-end across a well-defined product or business domain whilst maintaining a consistent user experience across the entire app. One of the key benefits of this approach is that it enables teams to be autonomous, working independently on deployable UI applications, reducing the risk of accidental coupling and defining clear boundaries around delimited contexts, minimising dependencies.

Exploring Architecture Patterns

Before jumping to realy kick start how it good implemenation module federation in micro-frontend, it better if speaking first the Monolithic, Microservices(Microfrontend in frontend side), Jamstack and Serverless Architecture. main true power of Next.js is flexibility, it allows every developer to create a wide variety of application small - continues - scale archhitecture design code. In lets discussing their strengths and weaknesses, and helping deciding in the future which approach is right for our project.

  • The monolithic architecture is a classic approach where the entire application is built as a single, cohesive unit. In the context of Next.js, this means that all pages, components, and API routes are managed within the same project.

  • Microfrontend architecture is a modular approach that breaks the application into smaller, self-contained services. Each service is responsible for specific functionality, and they communicate with each other using APIs.

  • Jamstack (JavaScript, APIs, and Markup) is a modern web development architecture that leverages static site generation (SSG) and APIs to create fast and scalable applications.

  • Serverless architecture focuses on using serverless functions, like Vercel or AWS Lambda, for handling the backend logic of your application.

Module federation making it possible

One of the module federation authors also created a library as a solution to allow Next.js applications to expose and consume remote modules. The example works like a charm when you run it… locally. The moment you want to deploy each application on separate hosts, it all falls apart. The problem is that the example relies on a local disk copy of the remote module when it is rendered on the server, which defeats the purpose of separate deployments for micro frontends.

It’s all about tradeoffs and if the server-side rendering of your remote modules isn’t critical, there’s a way to make it work. Let’s dive in. This is one of the common reference architectures of a micro frontends application:

Independently deployable apps are loaded in a single container to orchestrate a consistent user experience

Simply speaking a monolithic frontend application can be divided into smaller parts with separate repositories and independent build and deployment. As a result, there will be a Host application that consumes modules from smaller Remote applications.

In a typical micro frontend architecture utilizing Module Federation, each page may import multiple modules from remote applications. However, it is quite common for a page to import only one primary module that represents the complete page or a significant portion of it from a remote application.

Our components are simple

First Application

import Image from 'next/image'
import styles from '../styles/Mario.module.css'
const Mario = () => {
  return (
    <main className={styles.main}>
      <Image
        src="https://upload.wikimedia.org/wikipedia/en/a/a9/MarioNSMBUDeluxe.png"
        alt="Mario"
        width={240}
        height={413}
      />
      <h1 className={styles.title}>
        G'day! I'm Mario, a microfrontend.
      </h1>
      <span>I'm hosted at <a target="_blank" href="https://mf-micro-front-end-activate.vercel.app">https://mf-micro-front-end-activate.vercel.app</a></span>
    </main>
  )
}
export default Mario

Our next config file

const {
  withModuleFederation,
} = require("@module-federation/nextjs-mf");
module.exports = {
  future: { webpack5: true },
  images: {
    domains: ['upload.wikimedia.org'],
  },
  webpack: (config, options) => {
    const { isServer } = options;
    const mfConf = {
      mergeRuntime: true, //experimental
      name: "app2",
      library: {
        type: config.output.libraryTarget,
        name: "app2",
      },
      filename: "static/runtime/app2remoteEntry.js",
      remotes: {
      },
      exposes: {
        "./mario": "./components/mario",
      },
    };
    config.cache = false;
    withModuleFederation(config, options, mfConf);
    return config;
  },
  webpackDevMiddleware: (config) => {
    // Perform customizations to webpack dev middleware config
    // Important: return the modified config
    return config;
  },
};

Second Application

import Image from 'next/image'
import styles from '../styles/Luigi.module.css'
const Luigi = () => {
  return (
    <main className={styles.main}>
      <Image
        src="https://upload.wikimedia.org/wikipedia/en/7/73/Luigi_NSMBUDX.png"
        alt="Luigi"
        width={240}
        height={413}
      />
      <h1 className={styles.title}>
        G'day! I'm Luigi, a microfrontend.
      </h1>
      <span>I'm hosted at <a target="_blank" href="https://mf-micro-front-end-main.vercel.app">https://mf-micro-front-end-main.vercel.app</a></span>
    </main>
  )
}
export default Luigi

Next JS config

const {
  withModuleFederation,
} = require("@module-federation/nextjs-mf");
module.exports = {
  future: { webpack5: true },
  images: {
    domains: ['upload.wikimedia.org'],
  },
  webpack: (config, options) => {
    const { isServer } = options;
    const mfConf = {
      mergeRuntime: true, //experimental
      name: "app1",
      library: {
        type: config.output.libraryTarget,
        name: "app1",
      },
      filename: "static/runtime/app1RemoteEntry.js",
      remotes: {
      },
      exposes: {
        "./luigi": "./components/luigi",
      },
    };
    config.cache = false;
    withModuleFederation(config, options, mfConf);
    return config;
  },
  webpackDevMiddleware: (config) => {
    // Perform customizations to webpack dev middleware config
    // Important: return the modified config
    return config;
  },
};

Now we will get both components on shell container components

const {
  withModuleFederation,
} = require("@module-federation/nextjs-mf");
module.exports = {
  future: { webpack5: true },
  images: {
    domains: ['upload.wikimedia.org'],
  },
  webpack: (config, options) => {
    const mfConf = {
      name: "shell",
      library: {
        type: config.output.libraryTarget,
        name: "shell",
      },
      remotes: {
        app1: "app1",
        app2: "app2",
      },
      exposes: {
      },
    };
    config.cache = false;
    withModuleFederation(config, options, mfConf);
    return config;
  },
  webpackDevMiddleware: (config) => {
    // Perform customizations to webpack dev middleware config
    // Important: return the modified config
    return config;
  },
};

Import-Module in the container application

import dynamic from 'next/dynamic'
const RemoteLuigi = dynamic(
  () => import("app1/luigi"),
  { ssr: false }
)
const App2 = () => (<RemoteLuigi />)
export default App2

// another page 
import dynamic from 'next/dynamic'
const RemoteMario = dynamic(
  () => import('app2/mario'),
  { ssr: false }
)
const App1 = () => (<RemoteMario />)
export default App1

How to Test setup

cd micro-front-end-activate
npm install 
nbpm run build
npm run dev

cd micro-front-end-main
npm install
npm run build
npm run dev

cd micro-front-end-shell
npm install 
npm run build 
npm run dev

Testing setup

Module federation allows a JavaScript application to dynamically load code from another application — in the process, sharing dependencies, if an application consuming a federated module does not have a dependency needed by the federated code — Webpack will download the missing dependency from that federated build origin.

This demo was only to showcase how it works, I have created a demo also on the same Here

References:

https://dev.to/logrocket/micro-frontend-with-react-and-nextjs-n6h

https://alibek.dev/micro-frontends-with-nextjs-and-module-federation

https://medium.com/@gopesh.jangid/frontend-design-systems-microfrontend-vs-monorepo-343d2b764041