Kelvin O Omereshone

Developer. Teacher. Speaker.

← Back to blog

Implementing Module Federation in The Boring JavaScript Stack

Just in case you didn’t catch the news, I rolled out TBJS 0.4.0 yesterday, now bundled with the fully-featured Mellow templates.

Given my recent tinkering into Module Federation, I figured there’s no better moment to demonstrate its integration with TBJS than right after this release! So let’s have at it!

In this tutorial, you’ll learn:

Whether you’re looking to improve application performance, enhance team collaboration, or simply modernize your development workflow, this tutorial will equip you with the knowledge and skills to get your hands dirty with Module Federation in The Boring JavaScript Stack.

By the end of this tutorial, you’ll be able to transform your monolithic applications into flexible, distributed micro-frontends without sacrificing the simplicity and reliability of The Boring JavaScript Stack.

What is Module Federation

Initially developed by Zack Jackson as a Webpack 4 feature to share code at runtime, Module Federation has now evolved into a bundler-agnostic method for sharing code between JavaScript applications.

Module Federation is a powerful architectural technique that allows different parts of your application to be developed, deployed, and versioned independently.

Module Federation can help you:

Why You Should Care About Module Federation

When you employ Module Federation as an architectural pattern in your team you get the following benefits

A Quick Primer on Bundler Runtime

Before we discuss how Module Federation works, it’s important to understand the concept of a bundler’s runtime. A bundler is a tool that takes all the various files (JavaScript, CSS, images, etc.) in your application and “bundles” them into a smaller number of files that can be efficiently served to the browser.

The key part here is the “runtime” - this is the code that the bundler generates to manage the loading and execution of your application’s code at runtime. Bundlers like Webpack and Rspack all have their own runtimes that takes care of things like:

This bundler runtime is typically embedded into the final bundle that gets served to the browser. And it’s this runtime that gives bundlers the ability to do dynamic things like Module Federation.

Understanding the role of the bundler’s runtime is crucial to grasping how Module Federation works under the hood. Now let’s dive into how Module Federation leverages this runtime capability of bundlers.

How Module Federation Works

At a high level, Module Federations works by enabling your application to dynamically load code from remote sources at runtime.

Here’s how it works in simple terms:

  1. Defining Providers: In your application, you designate certain parts as “providers” - these are the pieces of your app that can be shared and loaded dynamically by other parts of your app.

  2. Exposing Modules: The providers expose specific modules or components that other parts of your app can access and use. This allows you to share functionality between different sections of your application.

  3. Consuming Providers: Other parts of your app, known as “consumers”, can then import and use the exposed modules from the providers. The consumer application dynamically loads the provider code when it’s needed, without having to build or deploy it together.

As earlier stated, Module Federation leverages the fact that bundlers have runtimes, this means that the bundler’s runtime can be used to dynamically load and execute code at runtime, even if that code was not originally part of the bundle.

This is the key ingredient that enables Module Federation. By exposing certain parts of the application as providers and allowing other consumer parts to dynamically load and use those providers, the application can be broken down into smaller, more independent federated modules.

For more indepth info on how Module Federation works, check out Zack Jackson’s Understanding Module Federation: A Deep Dive

Implementing Module Federation

Module Federation is supported by Rspack, the bundler used by the Rsbuild tool employed by Sails Shipwright. This means that any project scaffolded with The Boring Stack has the necessary components to implement Module Federation right out of the box.

Step 1: Scaffold projects

Let’s begin by scaffolding two projects - provider and consumer with the create-sails CLI tool of The Boring JavaScript Stack inside a directory called tbjs-module-federation. In your terminal run the below commands:

mkdir tbjs-module-federation && cd tbjs-module-federation
npx create-sails@latest provider --react &&  npx create-sails@latest consumer --react

Step 2: Common set-ups

Okay, let’s start with setup that both the provider and consumer needs project. Go into both projects and then run npm i to install dependencies.

Next, let’s install the @module-federation/enhanced the package that provides enhanced features for Module Federation 2.0 in both consumer and provider:

npm i @module-federation/enhanced -D

So now let’s open up both provider and consi,er in an editor and start changing some stuff that will let us start exposing federated modules.

Create assets/js/boostrap.js which will contain the code currently in assets/js/app.js and then replace the content of assets/js/app.js with the following in both consumer and provider:

import('./bootstrap')

And assets/js/bootstrap.js will now have:

import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'
import '~/css/main.css'

createInertiaApp({
  resolve: (name) => require(`./pages/${name}`),
  setup({ el, App, props }) {
    createRoot(el).render(<App {...props} />)
  },
})

The above is necessary because of the async boundary in Module Federation since the modules will be available at runtime not build time, dynamically loading the project with import() is necessary for Module Federation to work today.

Step 3: Setting up the provider

What we want to do in provider is expose all the userland components in assets/js/components/ and layout in assets/layouts/ so consumer can start consuming them.

Let’s import the ModuleFederationPlugin in config/shipwright and dependencies from package.json

const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack')
const { dependencies } = require('../package.json')

Then let’s add assetPrefix: true to shipwright.build

  dev: {
    assetPrefix: true,
  }

Finally let’s setup the ModuleFederationPlugin and expose the components and layouts. We will also specify that react, react-dom, and @inertiajs/react are all shared dependencies:

module.exports.shipwright = {
  build: {
    tools: {
      rspack: (config, { appendPlugins }) => {
        config.output.uniqueName = 'federation_provider'
        appendPlugins([
          new ModuleFederationPlugin({
            name: 'federation_provider',
            exposes: {
              './components/GoogleButton':
                './assets/js/components/GoogleButton.jsx',
              './components/InputEmail':
                './assets/js/components/InputEmail.jsx',
              './components/InputText': './assets/js/components/InputText.jsx',
              './components/InputPassword':
                './assets/js/components/InputPassword.jsx',
              './components/InputButton':
                './assets/js/components/InputButton.jsx',
              './layouts/AppLayout': './assets/js/layouts/AppLayout.jsx',
            },
            shared: {
              react: {
                singleton: true,
                requiredVersion: dependencies['react'],
              },
              'react-dom': {
                singleton: true,
                requiredVersion: dependencies['react-dom'],
              },
            },
          }),
        ])
      },
    },
  },
}

Note both the config.output.uniqueName and name property of the ModuleFederationPlugin are the same and we will use this name while setting up the consumer to be aware of this provider.

So config/shipwright.js of provider should look like this:

const { pluginReact } = require('@rsbuild/plugin-react')
const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack')
const { dependencies } = require('../package.json')
module.exports.shipwright = {
  build: {
    dev: {
      assetPrefix: true,
    },
    tools: {
      rspack: (config, { appendPlugins }) => {
        config.output.uniqueName = 'federation_provider'
        appendPlugins([
          new ModuleFederationPlugin({
            name: 'federation_provider',
            exposes: {
              './components/GoogleButton':
                './assets/js/components/GoogleButton.jsx',
              './components/InputEmail':
                './assets/js/components/InputEmail.jsx',
              './components/InputText': './assets/js/components/InputText.jsx',
              './components/InputPassword':
                './assets/js/components/InputPassword.jsx',
              './components/InputButton':
                './assets/js/components/InputButton.jsx',
              './layouts/AppLayout': './assets/js/layouts/AppLayout.jsx',
            },
            shared: {
              react: {
                singleton: true,
                requiredVersion: dependencies['react'],
              },
              'react-dom': {
                singleton: true,
                requiredVersion: dependencies['react-dom'],
              },
              '@inertiajs/react': {
                singleton: true,
                requiredVersion: dependencies['@inertiajs/react'],
              },
            },
          }),
        ])
      },
    },
    plugins: [pluginReact()],
  },
}

Now you can go ahead and start up the dev server for the provider by running

npm run dev -- --port 1338

Step 4: Setting up the consumer

So for the consumer, we will begin by importing ModuleFederationPlugin and dependencies from package.json

const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack')
const { dependencies } = require('../package.json')

Then we will make the consumer aware of the provider by setting up the ModuleFederationPlugin:

module.exports.shipwright = {
  build: {
    tools: {
      rspack: (config, { appendPlugins }) => {
        appendPlugins([
          new ModuleFederationPlugin({
            name: 'federation_consumer',
            remotes: {
              federation_provider:
                'federation_provider@http://localhost:1338/mf-manifest.json',
            },
          }),
        ])
      },
    },
  },
}

Finally we will specified the shared dependencies

  shared: {
    react: {
      singleton: true,
      requiredVersion: dependencies.react,
    },
    'react-dom': {
      singleton: true,
      requiredVersion: dependencies['react-dom'],
    },
    '@inertiajs/react': {
      singleton: true,
      requiredVersion: dependencies['@inertiajs/react'],
    },
  },

This is how config/shipwright.js in consumer should look like:

const { pluginReact } = require('@rsbuild/plugin-react')
const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack')
const { dependencies } = require('../package.json')

module.exports.shipwright = {
  build: {
    tools: {
      rspack: (config, { appendPlugins }) => {
        appendPlugins([
          new ModuleFederationPlugin({
            name: 'federation_consumer',
            remotes: {
              federation_provider:
                'federation_provider@http://localhost:1338/mf-manifest.json',
            },
            shared: {
              react: {
                singleton: true,
                requiredVersion: dependencies.react,
              },
              'react-dom': {
                singleton: true,
                requiredVersion: dependencies['react-dom'],
              },
              '@inertiajs/react': {
                singleton: true,
                requiredVersion: dependencies['@inertiajs/react'],
              },
            },
          }),
        ])
      },
    },
    plugins: [pluginReact()],
  },
}

Step 5: Using federated modules in consumer

Okay finally let’s now begin replacing components and AppLayout used in consumer to use the federated counterparts. So you will replace these imports in assets/js/pages/login.jsx:

import InputEmail from '@/components/InputEmail.jsx'
import InputPassword from '@/components/InputPassword.jsx'
import InputButton from '@/components/InputButton.jsx'
import GoogleButton from '@/components/GoogleButton.jsx'

With:

import InputEmail from 'federation_provider/components/InputEmail'
import InputPassword from 'federation_provider/components/InputPassword'
import InputButton from 'federation_provider/components/InputButton'
import GoogleButton from 'federation_provider/components/GoogleButton'

Note we don’t use the .jsx extension because the imports must match how we expose them in provider

Do these for all the usage in every file in assets/js/pages/ and then finally delete the content of assets/js/components/ and assets/js/layouts as we no longer need them as we are now using federated modules provided by provider 🚀

Now start up the dev server for consumer by running:

npm run dev

Now visit http://localhost:1337 to see the project using federated modules!

Step 6: Modifying Tailwind CSS

You might have noticed that the styling from provider no longer applies in consumer this is because Tailwind is not aware of provider codebase so it can crawl it to generate the final CSS.

To solve this we can either use Tailwind CSS safelist feature, which will need us to manually add classes to an array that should be generated whether Tailwind finds them in the content path or not.

However, since our setup is quite simple we can add the path to provider to the tailwind.config.js of consumer so the `content will now look like this:

 content: [
    './views/**/*.ejs',
    './assets/js/**/*.{js,ts,jsx,tsx}',
    '../provider/assets/js/**/*.{js,ts,jsx,tsx}'
  ],

And that’s it everything should now work as if those modules are in consumer. When you change any component in provider it will reflect in consumer. Pretty cool right?

See the full demo project

Learnings

I had so much fun setting this up and learnt a ton.

Next Steps

Alright, now that you know how to setup up Module Federation for your team in The Boring JavaScript Stack, what’s next? Well a couple of things:

I plan to blog about the above as sequels to this article so look out for those articles.

Newsletter