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
localhost:3000/mario will render Mario component from app2
localhost:3000/luigi will render luigi component from app1
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