Skip to content

Micro Frontend Architecture: Helping you move from theory to practice with our workshop

Micro frontends empower teams to work autonomously on specific parts of an application

In monolithic architectures, as applications grow in complexity, it may become increasingly difficult to maintain scalability, promote team collaboration, and ensure technology diversity.

Micro frontends aim to tackle these issues by breaking down the application into smaller, independently deployable components. 

This empowers different teams to work autonomously on specific parts of the application using the technologies best suited to their needs. By fostering independent deployment, facilitating maintenance and upgrades, and isolating failures, micro frontends provide a solution that promotes modularity, agility, and adaptability in the rapidly evolving landscape of web development. In this blog post, we will give you an overview of micro frontend architecture, focus on its most popular flavour called “module federation”, and provide you with a workshop that can be used to acquire some practical knowledge of the subject.

What is micro frontend architecture?

The concept of micro frontends was first mentioned circa 2016 as an extrapolation of microservices to the frontend realm. Since then, it has become a go-to strategy for splitting a monolithic frontend codebase into smaller pieces that can be owned, worked on and deployed independently.

What is module federation?

Module federation is one of the most popular approaches for implementing micro frontend architectures on either the client or the server side. With module federation, each micro frontend is treated as a standalone module that can be developed, deployed, and versioned independently. This allows those modules to share and consume each other’s functionality, resources, and components at runtime, improving collaboration and reusability.

Started as a Webpack Plugin, module federation has now evolved into a general concept adopted by other bundlers and frameworks, such as Vite and Rollup.

Other approaches

It’s fair to say that module federation, while being one of the most common modern approaches to implementing MFE architectures, is definitely not the only one. So, before we dive in into exploring various aspects of module federation, let’s quickly go over some other approaches that are worth mentioning:


Runtime web components

Each micro frontend is mounted at a custom HTML element, and the container performs instantiation.


Runtime JavaScript integration

Somewhat similar to both the previous approach and module federation, this one adds each micro frontend onto the page using a

Dedicated frameworks for MFE composition

One of the easiest ways to implement micro frontend architecture is to use a dedicated framework that takes care of all the ins and outs and lets you focus on the application code. Some notable examples of such frameworks are listed below:

Module federation glossary

Before we go any further, let’s establish the terminology we’ll be using while describing various aspects and flavours of module federation:

  • A host is an application that includes the initial chunks of our code, the ones that will be used to bootstrap our container. The concept of module federation assumes that some components the container will render are just being referenced to a remote and not the initial bundle, which allows for smaller bundle sizes and shorter initial load times.

  • A remote is a module that is being consumed by the host, and it can contain both shared components or common dependencies to be used by different hosts.

  • A bidirectional host is both a host and a remote, consuming other remotes and providing some code to other hosts.

Module federation with Webpack

As we mentioned earlier, initially module federation was implemented as a plugin introduced in Webpack 5.

To set up module federation in Webpack, you need to define the federated modules in your Webpack configuration files, specify the remote entry points and expose specific modules (aka “remotes” or “bidirectional hosts”) that you want to share with other applications. The remote entry points represent the Webpack builds that expose modules for consumption.

In the consuming application’s (aka “host” or “bidirectional host”) Webpack configuration, you define which federated modules you want to consume. You specify the remote entry points and the modules you want to import from those remotes.

When you build and run your applications, Webpack dynamically loads the federated modules at runtime. It fetches the remote entry points, resolves the requested modules, and injects them into the consuming application. This process allows you to share code between applications without physically bundling everything together.

Note that since an application can have multiple dependencies, a host can also have multiple remotes.

Shared dependencies

Shared dependencies refer to the libraries, frameworks, or modules that are required by multiple federated modules to function properly. By sharing these dependencies, modules can avoid duplication and ensure consistency and compatibility.

Shared dependencies typically include runtime libraries, such as React or Angular, along with any additional utility libraries or common components that are needed by the federated modules. They are typically declared and managed in a shared configuration, allowing modules to access and utilise them seamlessly.

It’s strongly recommended that you follow the guidelines from Zack Jackson (inventor and co-creator of module federation). Below are some key points from the guidelines:

  • Sharing should be done with care — since shared modules cannot be tree-shaken

  • If you need a singleton (like things that depend on React context), then it must be shared

  • Sharing all dependencies can lead to larger bundles, so it’s best to consider case-by-case

Shared API

Shared API, provided by Webpack 5, is described below:

  • Shared (object | [string]): an object or an array containing a list of dependency names that can be shared across the federated modules.

  • Eager (boolean): specifies whether the dependency will be eagerly loaded and provided to other federated modules as soon as the host app starts (otherwise will be loaded lazily when first requested by the federated app).

  • Singleton (boolean): whether the dependency will be considered a singleton, which means that only a single instance of it is supposed to be shared across all the federated modules.

  • RequiredVersion (string): specifies the required version of the dependency, which makes any incompatible version loaded separately (not shared) —  note that if the singleton property is set to true, setting requiredVersion will raise a warning in case of a conflict.

Consider the following example:

JavaScript
// webpack.config.js
const path = require('path');
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'MyApp',
      filename: 'remoteEntry.js',
      exposes: { './Button': './src/components/Button' },
      shared: {
        react: {
          singleton: true,
           requiredVersion: require('./package.json').dependencies.react,
        },
        'react-dom': {
          singleton: true,
           requiredVersion: require('./package.json').dependencies['react-dom'],
        },
      },
    }),
  ],
};


Here we have react and react-dom acting as shared dependencies. Both are required to be singletons (as it’s necessary for React to function properly), and the required versions must match the ones being specified in the application’s package.json file.

Federated types

When it comes to TypeScript applications, the most common problem with using external libraries (which can be federated remote modules) is that not all of them provide TypeScript types with the original code. In the context of module federation, this problem is aggravated by the fact that Webpack only loads resources from the federated modules at runtime. TypeScript, however, needs those during compilation.

Luckily, there are now several options that one can use to solve this problem:

Note that TypeScript plugins are the easiest way to handle federated types. They fetch the types at compile-time and store them within the project, in order to make them available to tsc when needed. Using TypeScript plugins means you can also make sure the types are synced whenever type-related changes happen on the remote side.

Federated tests

Writing federated tests for module federation with Webpack involves creating test scenarios that verify the seamless integration of federated modules across different applications. The main complexity, in this case, comes from the fact that module federation loads modules asynchronously and remotely at runtime, which may not work well with traditional testing approaches. Unit tests typically expect synchronous behaviour, but federated modules may not be loaded when the tests are executed.

As a solution to this, you can use Webpack’s  require.eager API, which forces Webpack to load federated modules synchronously during the test execution. This approach ensures that the modules are available when the tests run, enabling better unit testing of federated code.

For more information on writing federated tests, please refer to the original article by Zack Jackson.

Rounding things up with our workshop

Hopefully, now you are ready to move from theory to practice. To simplify this transition, we created a workshop with several exercises that help give you a practical overview of micro frontend architecture and, specifically, module federation with Webpack.

This workshop is made of multiple incremental modules (aka exercises), and each module builds on top of the previous one. At each step, you are asked to add features and solve problems. You will find the solution to each step in the src/step-{n}-{name} folder.

We hope you enjoy these exercises and share them with your friends and colleagues.

References

You may also like

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.

Contact