How Microfrontends Work: From iframes to Module Federation
Microfrontends are transforming how teams build and deploy frontend applications at scale. This tutorial explores the architectural landscape, from traditional approaches to modern Module Federation implementations. By the end, you'll be equipped to ...

Microfrontends are transforming how teams build and deploy frontend applications at scale. This tutorial explores the architectural landscape, from traditional approaches to modern Module Federation implementations.
By the end, you'll be equipped to evaluate whether microfrontends are the right solution for your team's specific needs.
I’ll cover the following:
What are Microfrontends?
If you've heard about microservices on the backend, microfrontends represent a similar approach in the frontend world, with many of the same benefits.
Your team might adopt a microfrontend approach to enable team autonomy, reduce deployment risks, and scale development across multiple teams. Each team owns its technology stack, deployment cadence, and workflows. Yet they still deliver a single, cohesive user interface.
The overall idea is to move away from a big monolithic UI to decoupled UI codebases that can be owned, managed, and deployed by separate teams independently.
The simplest way to think about Microfrontends is the following:
Integrate one piece of UI into another
What can this piece of UI be, you may ask? Here are some examples:
Pages – parts of a website owned by specific teams. For example, the Auth team may own login/signup pages, whereas the engagement team may own the marketing pages, and so on.
Components – Components like header and footer are good candidates for a microfrontend approach as well. They’re relatively static but need to stay consistent across the website and may integrate with teams who own different sets of pages.
Widgets – A recommendation widget may be owned by a recommendations team, for example, and it can be integrated into different parts of the page based on the context. This is different from a static component, as given the context, the recommendation widget may also fetch relevant data via APIs (also owned by the recommendations teams).
Traditional Microfrontend Patterns
After reading the definition of a microfrontend, you might be thinking, oh, wait, who builds UI with a big monolith these days anyway (except giants like Google)? If that’s the case, your team is most likely using one of these traditional approaches to building Microfrontends:
Server-Side Composition
This is the most common approach I've encountered across various organisations. The idea is to split your website based on route patterns or pages. For example, you might route users to the accounts team for any routes starting with /account/*
(/account/login
or /account/signup
may fall under this pattern). Or you may have a similar route prefix for other parts of your web app, like /blog/*
for the marketing section of your app.
This is typically implemented at the reverse proxy layer (such as using NGINX), which routes traffic to the appropriate downstream UI service based on the path matching.
iframes
Another common approach is using iframes, though this method has significant limitations.
Unlike server-side composition, which operates at the page level, iframes can integrate as widgets within pages. Using iframes, you can load another website as a part of the website you want to integrate it within using the tag.
Some examples of this approach, which you may have seen, are websites that integrate Twitter feeds, Google Maps, and so on. Although these are examples of external widget integrations with iframes, companies may integrate certain widgets that are powered through iframes.
Build Time Integration – Packages
This approach involves publishing components as a UI library that other applications can integrate.
This is useful if you want to integrate full-blown apps with multiple pages, widgets, or static components like headers and footers, where this approach is pretty common.
Typically, this approach means that one team publishes their components as a package, while other teams integrate a specific version of this package.
In this example, it’s important to note that the Widget component is pulled in during the dependency install phase of the app. The web app can utilise this widget like its own component, which gets built together as one module and shipped to the users.
Modern Microfrontend Patterns
Module Federation
Module Federation enables you to integrate remote UI pieces within a host application at runtime. These pieces can be full pages, widgets, or components.
Module Federation originated as a Webpack 5 feature, extending the bundler's capabilities to load JavaScript code from remote sources at runtime.
Module Federation 2.0 is the evolution/improvement of the original Webpack 5 feature, with implementations available for other popular bundlers like RSPack and Vite as well.
Even if you’re using Webpack 5, I would recommend using Module Federation 2.0 as it takes care of some common gotchas that exist in the original Webpack 5 implementation.
Let’s take an example to understand some of the common pieces of Module Federation.
Imagine that we’ve a blog application, owned by the Content Team & a Widget, which is owned by the Recommendations team.
Now, let’s say the content team wants to integrate a recommendation widget within their application. Assume these teams have separate codebases hosted on different domains. The content team is on website.com
& the recommendations team is on recommendation.com
Here’s how you can achieve this MFE integration via Module Federation:
Remote
Responsible for exposing JavaScript files as remote (for example, utilities, components, and so on).
In our example, it would be the Recommendation’s team acting as a remote & would require a configuration to ‘expose’ the Widget.
new ModuleFederationPlugin({
name: 'recommendation',
exposes: {
'./Widget': './src/Widget.js',
}
})
Remote Entry
Remote entry is the URL for the entry point for a remote. A remote may expose multiple JavaScript files, & remoteEntry file would be aware of all of them.
Module Federation by default hosts the remote entry file at the root. In our example, recommendation teams might host their remote entry on https://recommendation.com/remoteEntry.js
Host
An independent website that consumes JavaScript from one or more remotes via Remote Entry. Think of remote entry as a namespace for your app under which it can export multiple things like components, utils, and so on, as exposed by a particular remote.
In our example, the Content Team would act as a Host & they’ll define the recommendation team’s remote entry within remotes configuration.
new ModuleFederationPlugin({
name: 'content-blog',
remotes: {
"recommendation": 'recommendation@https://recommendation.com/remoteEntry.js',
},
// ... other configs
})
Shared
Both hosts and remote can specify dependencies as SemVer that are automatically negotiated and shared during runtime. These can include common framework dependencies, such as React, which may require being a singleton, or other vendor libraries that can be potentially shared.
Having the right shared configuration ensures that the client does not download libraries or code that is already available on the host when fetching UI pieces from a remote location, which is key for optimal performance when integrating Module Federation.
const deps = require("./package.json").dependencies;
new ModuleFederationPlugin({
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
}
},
// ... other configs
})
Imports and Usage
Module Federation integration lets you use imports as if those JS files were available locally. Module Federation does all the stitching behind the scenes at runtime, in terms of fetching the remote entry and appropriate dependencies to make it available when you use it.
// Import is of the format - /
import Widget from 'recommendation/Widget';
// Render somewhere, making sure to handle loading via Suspense
// & errors via error boundary in React
}
In a nutshell, the module federation concept is this simple —
Fetching JS code (components, utils, and so on) from a remote server at runtime and still being able to share dependencies and be performant while doing so.
Single SPA
When you look up microfrontends, Single SPA often appears as a popular solution. But its primary use case is quite specific: integrating components across multiple frameworks (for example, React + Angular + Vue in the same application). Here's how it works in practice:
Single SPA acts as a JavaScript router that mounts and unmounts entire applications based on URL routes. Each "single-spa application" is a framework-specific app that gets loaded when its route becomes active.
// Register applications with Single SPA
registerApplication({
name: '@mycompany/react-app',
app: () => System.import('@mycompany/react-app'),
activeWhen: ['/react-app']
});
registerApplication({
name: '@mycompany/angular-app',
app: () => System.import('@mycompany/angular-app'),
activeWhen: ['/angular-app']
});
Single SPA handles the "orchestration" part – deciding which app should be active and managing their lifecycles. It doesn't solve the "how do I load remote code" problem – you still need to pair it with one of the approaches we've discussed (Module Federation, build-time packages, and so on).
If your applications use the same framework (like all React), you can skip Single SPA entirely and use Module Federation directly. Single SPA adds complexity that's only justified when you truly need multi-framework integration.