Micro Front-Ends with Webpack’s Module Federation

Bahadır Avcı
ÇSTech
Published in
9 min readDec 20, 2022
https://www.freepik.com/free-photo/stacking-wooden-blocks-is-risk-creating-business-growth-ideas_6170458.htm

In this article, I will share my experiences migrating our Backoffice Front-End setup from Monolithic to Micro Front-Ends with Webpack’s Module Federation in @ÇSTech.

In the face of the disadvantages brought by the monolith structure, we decided to make our new projects with a new architecture. After a long research process, we decided that the Webpack’s Module Federation plugin is suitable for us.

Let’s take a look at how we decided on this in the following sections below 👇

What is Monolithic? 🤔

Showing differences between the two approaches.

Monolithic Architecture is the traditional way for software applications. It is an all-in-one architecture where all aspects of the software operate as a single unit. In other words, a single code base where the whole team merges the code and a single pipeline where everything is deployed at once and most probably tested at once.

This approach works great for medium-scale projects but when it becomes complex, functionality requirements change or the team is too small or too large to handle development demands, the monolith can become unbearable.

As in our case; difficulties in handling demands, rising maintenance costs, etc. pushed us to Micro Front-End.

Let’s take a look at Micro Front-Ends & Webpack’s Module Federation in the next section.

What is the Micro Front-Ends? 🤔

An image that describes the general overview and working logic of Micro Front-Ends.

Micro Front-End Architectures decompose a Front-End app into individual, semi-independent fragments working loosely together. This can help make large projects more manageable, e.g. when transitioning from legacy codebases.

Take a Backoffice application as an example. You can think of a shell project consisting of fragment applications such as the Products app, Orders app, etc. Now depending on the size of your company, you can separate your team into smaller, different domains or teams to handle these fragment applications individually.

Here are some of its advantages:

  • Splitted, maintainable, high-performance codebases,
  • Ability to scale development by working in parallel,
  • Ability to upgrade, update, or even rewrite parts of the Front-End incrementally and independently, without impacting the entire system,
  • Possibility of using different technologies.

What has the Micro Front-End brought us? 💪

Bird’s eye view of between Apps & UI concepts on Monorepo and Micro Front-Ends.

While transforming our projects from Monolith to Micro Front-Ends, we also changed the packages that we used to a new structure, which is called Monorepo. Now we are using Lerna for managing our monorepo structure.

After adopting the Micro Front-Ends & monorepo mentalities, this method has given us:

  • More effective dependency management,
  • More convenient code sharing,
  • Easy to run everything locally,
  • Partial code deployment,
  • Same code styles,
  • Quick debugging,
  • Flexible code sharing between libraries, components,
  • Manageable and purposeful CI/CD process.

What is Module Federation?

Simple Hybrid modeling of Shell and Fragment application.

Simply, Webpack’s Module Federation makes sharing code and dependencies between different code bases easier.

Module Federation Architecture loads the code dynamically at runtime to reduce the frequency of code duplication, and the host application only downloads the missing dependencies. Also, its shared option is very good at preventing and minimizing dependency duplication.

We can list the reasons for using Module Federation as follows:

  • Allowing different teams to work simultaneously on a larger application by building and deploying independent, split projects,
  • Increases build performance,
  • Supports lazy loading to load module bundles only when necessary, resulting in better web performance (Custom lazy load with Intersection Observer may be a different option),
  • The shared option minimizes & prevents dependency duplication, as the remotes depend on the host’s dependencies.
  • Manages dependencies by shared dependencies and helps download necessary dependencies even when there is an issue like a network failure.

Let’s jump to the demo part 🚀

Implementation

We will start by creating Shell app for our demo project. This app will be a wrapper of our fragment apps.

Our demo project will have one Shell application and two fragment applications called Shopping Cart and Product List.

Note: I will be using Typescript for all code examples.

First, create a React App with Typescript:

yarn create react-app shell-app --template typescript

cd app-shell

yarn add webpack webpack-cli webpack-server html-webpack-plugin css-loader style-loader babel-loader webpack-dev-server

Create a file whose name is bootstrap.tsx under the /src folder for bootstrapping the whole app:

import App from './App';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';

const container = document.getElementById('root');
const root = createRoot(container);

root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);

Let's add our bootstrap.tsx to index.ts folder:

import('./bootstrap');

A question may be asked here, such as “Why are we doing this?
We bootstrapped our shell app because of the asynchronous loading of the entire application. We have to make sure that all apps are installed and ready. We will repeat the same action in all apps even if it is a fragment or shell, it doesn’t matter.

We continue by creating a new file called webpack.config.js to project root:

const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
mode: 'development',
devServer: {
port: 4000
},
entry: './src/index.ts',
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
module: {
rules: [
{
test: /\.(scss|css)$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(js|jsx|tsx|ts)$/,
use: ['babel-loader', 'ts-loader'],
exclude: /node_modules/
}
]
},
optimization: {
usedExports: true
},
plugins: [
new CopyPlugin({
patterns: [{ from: './src/assets', to: 'assets' }]
}),
new ModuleFederationPlugin(
{
name: 'SHELLAPP',
filename: 'remoteEntry.js',
shared: [
{
react: { requiredVersion: deps.react, singleton: true },
'react-dom': {
requiredVersion: deps['react-dom'],
singleton: true,
},
...deps
},
],
}
),
new HtmlWebpackPlugin({
template:
'./public/index.html',
}),
]
};

As you see, we created an instance called ModuleFederationPlugin at the end of the config file. The only thing that is specific on the file and we specify the Module Federation specifications here.

To elaborate a little further:

  • name: An option that we use to make our app known to the other apps.
  • filename: This hint allows the URL where our shell or fragment applications are accessible after the build process and shell or fragment will be available at SHELLAPP@http://{domain}/remoteEntry.js
  • shared: This hint is used to determine which applications will be available to share. We will be deploying our shell or fragment application through this feature.
  • singleton: This hint only allows a single version of the shared module in the shared scope (disabled by default). Some libraries use a global internal state (e.g. react, react-dom). Thus, it is critical to have only one library instance running at a time.

Let’s shape it a little to make it clear that src/App.tsx file is a shell file:

import React from 'react';
import './App.css';

function App() {
return (
<div className="shell-page">
<h1>Shell App</h1>
</div>
);
}

export default App;

Let’s define a style for a shell app like this in src/App.css file:

.shell-page {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 0 auto;
padding: 10px;
width: 100%;
max-width: 960px;
height: auto;
text-align: center;
border: 1px solid #e00c0c;
border-radius: 4px;
background-color: #fdd1d1;
}

Let’s repeat the same steps for Shopping Cart and Product List applications (don’t forget UI specifications in App.tsx and App.css), and then define the following port specification at the top of the fragment’s webpack.config.js like this:

devServer: {
port: 5000
}

All right👌 We have finished the implementations so we can run our applications separately with the following command:

yarn webpack server

We should see the result like this:

We ran all our Micro Front-End applications separately without any problems 💪

Now we can run the fragment applications that we will expose within the main shell application.

When we look at the Module Federation documentation, two things stand out: exposes and remotes.

  • exposes: This hint allows us to share a component, a page, or the whole app from a fragment app. Module Federation uniquely versions the part you share. Thus, you will not have a cache or version problem when you want to use the fragment.
  • remotes: This hint allows us to communicate to the shell application which fragment, page, or application to get from the remote.

Let’s expose our Shopping Cart app and call it in our Shell Application from remotes.

To expose our Shopping Cart app, we need to make a change to our ModuleFederationPlugin instance in our webpack.config.js like this:

new ModuleFederationPlugin(
{
name: 'SHOPPINGCART',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App',
},
shared: [
{
react: { requiredVersion: deps.react, singleton: true },
'react-dom': {
requiredVersion: deps['react-dom'],
singleton: true,
},
...deps
},
],
}
),

To access the fragment on the remote, the changes we will make in the webpack.config.js file should be as follows:

new ModuleFederationPlugin(
{
name: 'SHELLAPP',
filename: 'remoteEntry.js',
remotes: {
SHOPPINGCART 'SHOPPINGCART@http://localhost:5001/remoteEntry.js'
},
shared: [
{
react: { requiredVersion: deps.react, singleton: true },
'react-dom': {
requiredVersion: deps['react-dom'],
singleton: true,
},
...deps
},
],
}
),

Our final touch will be to src/App.tsx file in the Shell App. To show the Shopping Cart fragment on the UI side we will use Dynamic Import & React Lazy:

import React from 'react';
import './App.css';

const ShoppingCartApp = React.lazy(
() => import('SHOPPINGCART/App')
);

function App() {
return (
<div className="shell-page">
<h1>Shell App</h1>
<React.Suspense fallback='Shopping Cart Loading...'>
<ShoppingCartApp />
</React.Suspense>
</div>
);
}
export default App;

And, that’s it 👌

We have easily called and used our Fragment apps from our Shell app. We made our examples on React, but we don’t have to. As I mentioned above, it should not be forgotten that Module Federation can gather applications written in different frameworks under a single roof. So you can think of each of these applications as a different framework.

Conclusion

We saw the architecture that we built on the Çiçeksepeti Backoffice side as a small demo. If you have a self-sufficient project or are in a migration process, you can get more successful results by blending with issues such as CI/CD processes and Micro Service Architecture in your Back-End. In this way, you will be able to automate and dynamically use the examples that we have prepared above.

Thank you for reading my article 🙏

If you have any questions about Webpack Module Federation or Micro Front-End Architecture, please feel free to ask.

You can reach me through the following channels:

Linkedin: https://www.linkedin.com/in/ali-bahadir-avci/
GitHub: https://github.com/Plakumat
Codepen: https://codepen.io/kraftworkz

Have a nice day,

Ali Bahadır Avcı
Senior Front-End Developer @ÇSTech

--

--