Let's Build Micro Frontends with NextJS and Module Federation!
Tags: javascript, nextjs, webpack, modulefederation
Published: 2020-11-24 18:55:49

That headline is a mouth-full, I know! 

In the past several years I have been working on distributed and multiple teams as well as being a pretty early adopter of NextJS (since around V2.0!) in production. I've worked on micro frontends with shared `npm` packages while trying to orchestrate one cohesive user experience. 

It was and is hard.

That’s why I have been closely following the latest developments in the field, and since I’ve heard about Webpack 5 Module Federation, I was curious how and when it would work with an amazing framework such as NextJS.

I guess the title and all those buzzwords need a little breakdown and explaining before we get down to business, so… here we go!

## What are Micro Front Ends?

Micro Front Ends are like microservices for the front end. Think about it as an encapsulated, self-contained piece of code or component that can be consumed anywhere. To quote [micro-frontends.org](https://micro-frontends.org):

> "*The idea behind Micro Frontends is to think about a website or web app as a composition of features which are owned by independent teams. Each team has a distinct area of business or mission it cares about and specializes in. A team is cross functional and develops its features end-to-end, from database to user interface."*

![https://microfrontends.com/img/deployment.png](https://microfrontends.com/img/deployment.png)

Source: [https://micro-frontends.org/](https://micro-frontends.org/)

You can read more about this concept in the provided link above or [here](https://martinfowler.com/articles/micro-frontends.html). The key core concepts to remember:

- Technology agnostic
- Isolated Team Code
- Build a resilient site / App

There are several frameworks and approaches to implement this architecture, but this is not the subject of this post. I will be focusing on sharing code.

## What's Module Federation?

Technically speaking, Module Federation is a [Webpack v5 feature](https://webpack.js.org/concepts/module-federation/) which allows separate (Webpack) builds to form a single application. However, it's much more than that...

To paraphrase Zack Jackson (don't remember where I heard it or saw it), one of the creators of Module Federation: 

> It is a distributed application architecture. Independently deployed bundles working as a monolith at runtime.

So, in a few bullet points:

- It’s a type of JavaScript architecture.
- It allows a JavaScript application to dynamically load code from another application
- It allows haring 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.
- Orchestrated at runtime not build time - no need for servers - universal

Module Federation is a tool based approach to implementing micro front-end architecture.

It is important not to confuse `Module Federation` with Webpack `[DllPlugin](https://webpack.js.org/plugins/dll-plugin/)` which is a tool mostly focused on [improving build time performance](https://webpack.js.org/plugins/dll-plugin/). It can be used to build apps that depend on DLLs (Dynamic Link Library), but this can cause deploy delays, there is the extra infrastructure for compile-time dependency, it needs to rebuild when parts change (which causes deploy delays), and it is highly dependent on external code with no fail-safe. In summary, DLLs don't scale with multiple applications and require a lot of manual work for sharing.

Module Federation, on the other hand, is highly flexible while allowing only less deploy delay due to needing only the shared code and app to be built. It is similar to [Apollo GraphQL federation](https://www.apollographql.com/docs/federation/) but applied to JavaScript modules - browser and `Node.js`.

Some terminology that is useful to know when talking about Module Federation:

- **Host: A Webpack build that is initialized first during a page load**
- **Remote: Another Webpack build, where part of it is being consumed by a “host”**
- **Bidirectional-hosts: can consume and be consumed**
- **Omnidirectional-hosts: A host that behaves like a remote & host at the same time**

I could blabber on a lot more about this, but if you want to learn more you can visit the [official website](https://module-federation.github.io/), you can get the ["Practical Module Federation" book](https://module-federation.myshopify.com/products/practical-module-federation), or you can check out the resources section.

## What's NextJS?

If you're not familiar with the frontend/React ecosystem or you have been living under a rock, NextJS is a React framework for building hybrid static and server-side rendered React application. 

Basically, it takes off a lot of the hassle of configuring, tinkering, and retrofitting what it takes to get a React application (or website) to Production.

It has a large variety of features out of the box that just makes any web developer grin like a giddy school girl. 

To name a few key features:

- Zero configuration
- TypeScript Support
- File-system routing
- Built-in serverless  functions (AKA API routes)
- Code splitting and bundling

> You can also hear a little bit about what NextJS is in a [talk I gave](https://youtu.be/srueFUnJ8Jo?t=402) (my first one! pardon my nervousness there 😅) - keep in mind this was a year and a half ago from the time of this writing and the NextJS team have added a whole lot of features and optimizations since.

For the sake of this post, it is important to remember that frameworks have limitations and in this tutorial, we are fighting some of the limitations NextJS has. The team behind NextJS has made incredible strides in a short period of time. However, to be able to use Module Federation we will need to work around some key aspects, such as no Webpack v5 support (yet) and the framework is not fully async.

## What are we going to build?

We're going to build 2 Next JS apps: 

1. Remote App (App 1)- will expose  a React component and 2 functions
2. Consumer (App 2) - will consume code/components from the first app.

> You can also watch [this great video by Jack Herr](https://www.youtube.com/watch?v=vX6EXi5I9LE) if you're more into that.

If you want to skip all of this and see all of the code, [here's a link to the repo](https://github.com/hamatoyogi/next-mf-blogpost).

So.. after that's out of our way...

## Let's do it!

[https://gph.is/2Nek1o9](https://gph.is/2Nek1o9)

## First Steps:

- Create a folder to hold both apps.
- To kick start the first app go into the created folder and run :

```bash
npx create-next-app app1 
```

- Kick start the second (notice that this time its `app2`):

```bash
npx create-next-app app2 
```

Ok, now we should have 2 apps with NextJS with a version that should be `^9.5.6`.

If you want to stop and try to run them to see they work, just go to their folders and start them off with:

```bash
yarn run dev
```

Now, in order to use Module Federation, we need Webpack v5, but alas, at the time of this writing Next's latest version still runs Webpack 4. 😢

But don't panic yet! Luckily for us, our friend Zack has us covered with a little nifty package for this transition period called `@module-federation/nextjs-mf`!

## Setting up our remote app:

### Step 1

Go into `app1` and run:

```bash
yarn add @module-federation/nextjs-mf
```

### Step 2

In order to use Webpack 5 with our Next apps we're going to need to add resolutions to our `package.json`:

```jsx
"resolutions": {
    "webpack": "5.1.3"
  },
```

What this does is tell our package manager to use this specific version of Webpack we want to use. But because we've used `create-next-app` to bootstrap our app, we now need to clean up our `node_modules`:

```jsx
// in the same folder for app1 delete node_modules:
rm -rf node_modules

// re-install all of our pacakges, but this time Webpack 5 should be installed:
yarn install
```

Our boilerplate code is almost ready. What we are missing at this point are the modules we would want to expose to our consumer app.

Let's add some.

### Step 3

First we'll create just a simple `Nav` component:

```jsx
import * as React from 'react';

const Nav = () => {
  return (
    <nav
      style={{
        background: 'cadetblue',
        width: '100%',
        height: '100px',
        color: 'white',
        textAlign: 'center',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        fontSize: '24px',
      }}>
      Nav
    </nav>
  );
};

export default Nav;
```

Now just to make sure it's working we'll add it to our `index.js` page and see it render:

```jsx
import Nav from '../components/nav'

export default function Home() {
  return (
    <div className={styles.container}>
			{/* JSX created by create-next-app */}
      <main className={styles.main}>
        <Nav />
			{/* mroe JSX created by create-next-app */}
			</main>
		</div>
	)
}
```

If we run `yarn dev` in `app1` folder and go to `localhost:3000` we should see something like this:

![app1-start](https://dev-to-uploads.s3.amazonaws.com/i/io7xv7o91piinhvo5uo2.png)

### Step 4

We'll add two functions to export as well:

```jsx
// utils/add.js

const add = (x,y) => {
    return x + y;
}

export default add

// utils/multiplyByTwo.js

function multiplyByTwo(x) {
    return x *  2;
}

export default multiplyByTwo
```

### Step 5

After these steps we should be able to use configure our Module Federation Webpack plugin. So, we need to create a `next.config.js` file in the root folder and add this:

```jsx
const {
  withModuleFederation,
  MergeRuntime,
} = require('@module-federation/nextjs-mf');
const path = require('path');

module.exports = {
  webpack: (config, options) => {
    const { buildId, dev, isServer, defaultLoaders, webpack } = options;
    const mfConf = {
      name: 'app1',
      library: { type: config.output.libraryTarget, name: 'app1' },
      filename: 'static/runtime/remoteEntry.js',
      // This is where we configure the remotes we want to consume.
      // We will be using this in App 2.
      remotes: {},
      // as the name suggests, this is what we are going to expose
      exposes: {
        './nav': './components/nav',
        './add': './utils/add',
        './multiplyByTwo': './utils/multiplyByTwo',
      },
      // over here we can put a list of modules we would like to share
      shared: [],
    };

    // Configures ModuleFederation and other Webpack properties
    withModuleFederation(config, options, mfConf);

    config.plugins.push(new MergeRuntime());

    if (!isServer) {
      config.output.publicPath = 'http://localhost:3000/_next/';
    }

    return config;
  },
};
```

### Step 6

Next, we need to add `pages/_document.js`:

```jsx
import Document, { Html, Head, Main, NextScript } from "next/document";
import { patchSharing } from "@module-federation/nextjs-mf";

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html>
				{/* This is what allows sharing to happen */}
        {patchSharing()}
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
```

---

**Side note :** 

for easing this process it is possible to install `@module-federation/nextjs-mf` globally (`yarn global add @module-federation/nextjs-mf`) and from `app2` folder run:

```jsx
nextjs-mf upgrade -p 3001
```

This will setup up your `package.json` , `_document.js`, and `next.config.js` from the exposing app set up steps (2, 5, 6) as well as set up the running script for this app to run on `PORT:3001` to avoid port clashes.

However, the caveat of this method (at the time of this writing) is that for some reason this changes our NextJS version and `nexjs-mf` package version to older ones (in `package.json`):

```jsx
{
  "name": "app2",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev -p 3001",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "^9.5.6-canary.0",
    "react": "17.0.1",
    "react-dom": "17.0.1",
    "@module-federation/nextjs-mf": "0.0.1-beta.4"
  },
  "resolutions": {
    "webpack": "5.1.3",
    "next": "9.5.5"
  }
}
```

Just be aware if you use this method. 

---

## Setting up our consumer app:

If you've opted out of using the above method, make sure you're `package.json` looks like this:

```jsx
{
  "name": "app2",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev -p 3001",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "10.0.2",
    "react": "17.0.1",
    "react-dom": "17.0.1",
    "@module-federation/nextjs-mf": "0.0.2"
  },
  "resolutions": {
    "webpack": "5.1.3"
  }
}
```

Then we need to repeat the same steps as in Step1 and Step2 from the exposing app (add resolutions, remove `node_modules` and reinstall), just make sure you're  targeting `app2` folder.

Next, create your `next.config.js`:

```jsx
const {
  withModuleFederation,
  MergeRuntime,
} = require('@module-federation/nextjs-mf');
const path = require('path');

module.exports = {
  webpack: (config, options) => {
    const { buildId, dev, isServer, defaultLoaders, webpack } = options;
    const mfConf = {
      name: 'app2',
      library: { type: config.output.libraryTarget, name: 'app2' },
      filename: 'static/runtime/remoteEntry.js',
      // this is where we define what and where we're going to consume our modules.
      // note that this is only for local development and is relative to where the remote
      // app is in you folder structure.
      remotes: {
        // this defines our remote app name space, so we will be able to
        // import from 'app1'
        app1: isServer
          ? path.resolve(
              __dirname,
              '../app1/.next/server/static/runtime/remoteEntry.js'
            )
          : 'app1', // for client, treat it as a global
      },
      exposes: {},
      shared: [],
    };

    // Configures ModuleFederation and other Webpack properties
    withModuleFederation(config, options, mfConf);

    config.plugins.push(new MergeRuntime());

    if (!isServer) {
      config.output.publicPath = 'http://localhost:3001/_next/';
    }

    return config;
  },
};
```

Then add `_document.js`:

```jsx
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { patchSharing } from '@module-federation/nextjs-mf';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html>
        {patchSharing()}
        {/* This is where we're actually allowing app 2 to get the code from app1 */}
        <script src="http://localhost:3000/_next/static/remoteEntryMerged.js" />
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
```

Now we can start consuming modules from app1! 🎉🎉🎉

![great success](https://dev-to-uploads.s3.amazonaws.com/i/qn7sr75x49hrqlag0twf.jpeg)
 
Let's import those modules in our `pages/index.js`:

```jsx
// We need to use top level await on these modules as they are async. 
// This is actually what let's module federation work with NextJS
const Nav = (await import('app1/nav')).default;
const add = (await import('app1/add')).default;
const multiplyByTwo = (await import('app1/multiplyByTwo')).default;

export default function Home() {
  return (
    <div className={styles.container}>
			{/* JSX created by create-next-app */}
      <main className={styles.main}>
        <Nav />
				<h2>
          {`Adding 2 and 3 ==>`} {add(2, 3)}
        </h2>
        <h2>
          {`Multiplying 5 by 2  ==>`} {multiplyByTwo(5)}
        </h2>
			{/* mroe JSX created by create-next-app */}
			</main>
		</div>
	)
}
```

Let's check that everything works as expected:

```jsx
// run in /app1 folder, and then in /app2 floder:
yarn dev
```

Go to your browser and open `[localhost:3001](http://localhost:3001)` (app2) and this is what you should see:

![app2-share](https://dev-to-uploads.s3.amazonaws.com/i/et49my0gfud8jhivxmjb.png) 

We were able to consume a component and 2 modules from `app1` inside of `app2`! 🚀🚀🚀

This is where some more magic comes in:

- Go to `app1/nav` and change the `backgroundColor` property to something else like `hotpink` and hit save.
- Stop `app2` server and rerun it with `yarn dev` again

If you refresh `[localhost:3001](http://localhost:3001)` you should see this result:

![app2-updated](https://dev-to-uploads.s3.amazonaws.com/i/pkmf59jqtagzv2gjjt5u.png) 

![https://media.makeameme.org/created/what-just-happened-391ydu.jpg](https://media.makeameme.org/created/what-just-happened-391ydu.jpg)


What happened here? We were able to simulate a code change in `app1` that was received in `app2` without making any changes to the actual code of `app2`!

## Issues and Caveats Along  The Way

When I first started off playing around with this setup I ran into an issue where I got a blank screen on the consumer app, apparently, it was due to the naming of my apps and folders. I've even [opened up an issue about this](https://github.com/module-federation/nextjs-mf/issues/21) in the `next-mf` package. In short, Don't use kebab case names and pay attention to the file paths 🤷🏽 🤦🏾.

Another important note is that exposing components and pages as modules works well, but there are issues when you try to use NextJS `Link` component.

Lastly, note that you cannot expose `_app.js` as a shared module.

## Deployment

I thought it would be cool to see this project running in a production environment, so I went on and tried to deploy the two apps to 2 popular cloud hosting services:

**[Vercel](https://vercel.com/)** - ****Attempted to deploy there, didn't work due to Webpack 5 resolutions and a clash in the platform. I have opened a ticket in their support system but still have yet to resolve the issue.

**[Netlify](https://www.netlify.com/)**  - As it is, Netlify only support sites to be deployed with the JAMStack architecture, so it only supports NextJS with [static HTML export](https://nextjs.org/docs/advanced-features/static-html-export). When running a build locally, I was able to get both apps working while sharing modules even when using `next export` - the important file `remoteEntryMerged.js` was created in the `.next` build folder: 

![remote-file-in-directory](https://dev-to-uploads.s3.amazonaws.com/i/qkx2mlkclupvi0sgi65s.png) 

However after deploying with the correct environment variables in place, for some reason that file is missing from the sources:

![remote-missing-source](https://dev-to-uploads.s3.amazonaws.com/i/h3dhwev2coaugqkk6cyt.png) 

Hopefully, I will be able to sort one of these out at some point. When and if I do, I will update. But as it seems, in order to get this sort of stack running in an actual production environment there is some tinkering to do. I do believe that if you try to just copy the build folder as it outputted locally to an S3 bucket or something similar, it should probably work.  

# Conclusion

In this post, we've seen how to set up and work with Module Federation and NextJS which allows us to share code and components, which in a way, is what allows micro frontends. 

This is probably only a temporary solution to get this set up working until NextJS upgrades to Webpack 5.

One thing to keep in mind with Module Federation and using this type of architecture is that it comes with a slew of challenges as well. How to manage versions of federated modules is still in it's early days, only a handful of people have actually used it in production. There is a solution being worked on by Zack Jackson (and  I'm helping out! 😎) called Federation Dashboard which uses "[Module Federation Dashboard Plugin](https://www.npmjs.com/package/@module-federation/dashboard-plugin)", but it's still in the making... 

Another challenge might be shared modules sharing breaking contracts or APIs with consuming apps.

Then again, these are solvable problems, just ones that haven't been iterated enough through yet.

I am a strong believer in the technologies and architecture I've touched on in this post and I'm excited to see what the future holds!

## Resources

[hamatoyogi/next-mf-blogpost](https://github.com/hamatoyogi/next-mf-blogpost)

[Module Federation for NextJS 10](https://www.youtube.com/watch?v=vX6EXi5I9LE)

[@module-federation/nextjs-mf](https://www.npmjs.com/package/@module-federation/nextjs-mf)

[WP5-nextjs.md](https://gist.github.com/jherr/dd10bc34448590a2661a0c6acbcc7e8a)

[Module Federation in Webpack 5 - Tobias Koppers](https://www.youtube.com/watch?v=gmUm7CTsNhk)

[Webpack 5 Module Federation - Zack Jackson - CityJS Conf 2020](https://www.youtube.com/watch?v=-ei6RqZilYI)

[Introducing Module Federation](https://www.youtube.com/watch?v=x22F4hSdZJM)