Microfrontends, Explained Without the Hype
What a microfrontend actually is, the real problem it solves (hint: it's about teams, not code), how it differs from a normal SPA or a monorepo, and a hands-on build where you stitch three independently-deployed apps into one page with Module Federation.
Published · by Frontend Masters India
You have probably heard "microfrontend" thrown around in an architecture meeting and nodded along. This article is the thing you wish someone had said in that meeting: what it actually is, the one problem it genuinely solves, and how to build a real one yourself by the end.
I am going to be honest with you throughout, including the part where I tell you that you probably should not use this.
What it is, in one breath
A microfrontend is one web page that is assembled at runtime from several separate frontend apps, each built and deployed on its own.
The user sees a single, normal-looking page. Under the hood, different chunks of that page were shipped by different teams, from different codebases, on different days.
What the user sees What is actually running
┌───────────────────────┐ ┌───────────────────────┐
│ [ nav bar ] │ │ shell app (team A) │
│ ┌─────────────────┐ │ │ ┌─────────────────┐ │
│ │ product grid │ │ ◀── │ │ catalog app (B) │ │ deployed separately
│ └─────────────────┘ │ │ └─────────────────┘ │
│ ┌─────────────────┐ │ │ ┌─────────────────┐ │
│ │ cart + checkout │ │ ◀── │ │ cart app (C) │ │ deployed separately
│ └─────────────────┘ │ │ └─────────────────┘ │
└───────────────────────┘ └───────────────────────┘
one URL, one page three apps, three teamsThe word borrows from "microservices," which did the same thing to the backend years ago: take one big service and split it into smaller ones that deploy independently. Microfrontends apply that idea to the browser.
The real problem it solves
Here is the part most explanations skip, and it is the only part that matters.
Microfrontends do not make your site faster. They do not make your code cleaner. They are not a better way to organise components. If anyone sells you on them for those reasons, walk away.
They solve exactly one problem: too many people working in one frontend codebase.
Picture a company with 60 frontend engineers across eight teams, all committing to one large React app. The pain shows up like this:
- One team's broken test blocks everyone else's deploy.
- A release goes out once a day because coordinating 60 people is slow, so your two-line fix waits 14 hours to ship.
- Someone upgrades a shared dependency and quietly breaks three other teams' features.
- The build takes nine minutes because it compiles everyone's code, even though you touched one file.
None of that is a code problem. It is a coordination problem. The teams are stepping on each other because they share one pipeline. Microfrontends cut the app along team boundaries so each team owns its slice end to end: its own repo, its own build, its own deploy, its own on-call.
That is why the people who actually run this in production tend to be large organisations, not small ones:
- Spotify splits the desktop and web client into independently-owned pieces.
- DAZN (sports streaming) runs separate teams for playback, browse, and account, each shipping on its own.
- IKEA and Zalando split their storefronts so the catalog, search, and checkout teams release independently.
- American Express uses it so payments and rewards teams do not block each other.
Notice the pattern. Every one of these is a company big enough that the teams were already colliding. The microfrontend was the fix for the collision, not a thing they reached for because it sounded modern.
How it differs from things you already know
Versus a normal single-page app
A normal SPA is one build. You change a button, you rebuild and redeploy the whole thing. Everyone's code ships together because it is all one bundle.
A microfrontend setup has several builds that come together in the browser. The cart team can ship a fix at 2pm without the catalog team rebuilding anything.
Versus a component library
A shared component library (your <Button>, <Modal>, design system) is code you import at build time. When the library releases a new version, every app that uses it has to upgrade, reinstall, and rebuild to get the change. The coupling is still there.
A microfrontend is loaded at runtime. The shell does not bundle the cart's code. It fetches it from a URL when the page loads, so the cart team's latest deploy shows up without the shell rebuilding at all.
component library microfrontend
───────────────── ─────────────
import at build time fetch at runtime
upgrade = reinstall upgrade = just redeploy that piece
one final bundle separate bundles, stitched liveVersus a monorepo
This one trips people up because they sound similar. A monorepo is many projects in one git repository. It is about where the code lives. You can have a monorepo that still produces one single deploy, which gives you none of the independence.
Microfrontends are about how the app deploys and runs, not where the code sits. You can do microfrontends from a monorepo or from many repos. The defining trait is independent deployment, not the folder layout.
The three ways people actually do it
There are really only three approaches you will meet in the wild.
1. iframes. The oldest trick. Each piece is a separate page embedded with <iframe>. It gives you bulletproof isolation: one app cannot touch another's styles or globals. But sharing state, sizing, routing, and a consistent look across iframe boundaries is genuinely painful, and the performance is rough. It still shows up for embedding third-party widgets, rarely for a whole product.
2. Build-time integration (npm packages). Each microfrontend is published as a package, and the shell installs them. Honest downside: this is not really independent deployment. To get a team's new version, the shell has to reinstall and rebuild. You have added complexity without buying the independence that was the whole point.
3. Runtime integration with Module Federation. This is what most modern setups mean when they say "microfrontend." Each app builds to a file the others can fetch over HTTP at runtime. The shell loads a remote app live, with no rebuild, and they can share a single copy of React instead of each shipping their own. This is the one worth learning, and it is what we will build next.
Module Federation started in Webpack 5 and now runs across Webpack, Rspack, and Vite through plugins. In 2026 it has grown to cover ESM and even server-side stitching of components from different origins, but the core idea below has not changed.
Build one yourself
Time to make this real. We will build a tiny storefront as three separate apps:
shell— the host. Owns the page shell and nav, and pulls in the other two.catalog— exposes a product grid.cart— exposes a cart widget.
Each runs on its own port, as if owned by its own team. The shell will load the other two at runtime. This is the e-commerce split that IKEA and Zalando actually run, shrunk to something you can finish in an afternoon.
Step 1: Scaffold three apps
The fastest way to get a working Module Federation skeleton is create-mf-app, which sets up the Webpack config for you. Run it three times:
npx create-mf-app
# answer the prompts:
# name: shell type: application port: 3000 framework: react
# then run it again for:
# name: catalog type: application port: 3001 framework: react
# name: cart type: application port: 3002 framework: reactYou now have three independent React apps. Nothing connects them yet. That is the point: each could be a different team's repo.
Step 2: Have catalog expose its product grid
A remote has to declare what it is willing to hand out. Open the catalog's webpack.config.js and find the ModuleFederationPlugin. The two fields that matter are filename (the manifest other apps will fetch) and exposes (what you make available):
new ModuleFederationPlugin({
name: "catalog",
filename: "remoteEntry.js",
exposes: {
// public name → the file inside this app
"./ProductGrid": "./src/ProductGrid",
},
shared: {
react: { singleton: true },
"react-dom": { singleton: true },
},
});singleton: true is the line that saves you. It tells Module Federation that React must exist exactly once on the page, shared across all three apps. Without it, the shell and each remote would each load their own React, and hooks would blow up the moment two copies meet.
Now write the component it points at, src/ProductGrid.jsx:
const products = [
{ id: 1, name: "Desk lamp", price: 24 },
{ id: 2, name: "Notebook", price: 6 },
{ id: 3, name: "Wool throw", price: 49 },
];
export default function ProductGrid() {
return (
<div>
<h2>Products</h2>
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name} — ${p.price}
</li>
))}
</ul>
</div>
);
}Do the same in cart: expose "./CartWidget": "./src/CartWidget" and write a small CartWidget.jsx. Same name: "cart" and same shared block.
Step 3: Have shell consume both remotes
The host declares where to find the remotes. In the shell's webpack.config.js:
new ModuleFederationPlugin({
name: "shell",
remotes: {
// local name → remoteName@URL-of-its-remoteEntry.js
catalog: "catalog@http://localhost:3001/remoteEntry.js",
cart: "cart@http://localhost:3002/remoteEntry.js",
},
shared: {
react: { singleton: true },
"react-dom": { singleton: true },
},
});Read that remotes map closely, because it is the whole trick. The shell does not import the catalog's source. It points at a URL. When the page loads, the browser fetches remoteEntry.js from port 3001, and that file knows how to hand over ProductGrid. Redeploy the catalog and the shell picks up the new version on the next load, no rebuild.
Step 4: Drop the remotes into the page
In the shell's src/App.jsx, load each remote lazily. They come over the network, so they have to be wrapped in Suspense:
import { lazy, Suspense } from "react";
// These imports resolve at runtime, not build time.
const ProductGrid = lazy(() => import("catalog/ProductGrid"));
const CartWidget = lazy(() => import("cart/CartWidget"));
export default function App() {
return (
<div>
<nav>My Store</nav>
<Suspense fallback={<p>Loading products…</p>}>
<ProductGrid />
</Suspense>
<Suspense fallback={<p>Loading cart…</p>}>
<CartWidget />
</Suspense>
</div>
);
}That import("catalog/ProductGrid") does not exist on disk in the shell. Webpack sees the catalog/ prefix, matches it against the remotes map, fetches the remote, and resolves the component. It looks like a normal import and behaves like a network call.
Step 5: Run all three
Start each app in its own terminal:
# terminal 1
cd catalog && npm start # serving on :3001
# terminal 2
cd cart && npm start # serving on :3002
# terminal 3
cd shell && npm start # serving on :3000Open http://localhost:3000. You will see one page with the nav, the product grid, and the cart. Now prove the point: change the product list in catalog/src/ProductGrid.jsx and save. The catalog rebuilds on its own port. Refresh the shell and the new products appear, even though you never touched or rebuilt the shell. That is independent deployment, working in front of you.
Using Vite instead of Webpack? The same config shape works through
@module-federation/vite. You add the plugin to each app'svite.config.jswith the samename,exposes,remotes, andsharedfields. The mental model does not change.
When NOT to use this
I promised honesty, so here it is. Most teams that adopt microfrontends should not have.
Adoption tells the story. Microfrontends were a loud trend a few years ago, and a lot of teams that jumped on them have since walked it back. The honest read is that most of them never had the problem in the first place: they had messy code or unclear ownership, and they hoped splitting the deploy would clean it up. It does not. You just get messy code spread across more pipelines, plus a pile of new problems:
- Each remote ships its own bundle, so total download size usually goes up, not down.
- Versioning shared dependencies across teams becomes a standing tax.
- A bug at the seams between apps is harder to debug than a bug in one codebase.
- Local development now means running several apps at once.
So before you split anything, ask one question: are multiple teams actually blocking each other in one frontend codebase today?
If the answer is no, you do not have the problem this solves. A well-organised single app, or a modular monolith with clear internal boundaries, will serve you better and cost you far less. Reach for microfrontends when the coordination pain is real and measurable, not because the architecture diagram looks impressive.
The one-line version
A microfrontend lets separate teams ship parts of one page independently. It is an answer to an organisational scaling problem, and a poor answer to anything else. Learn it so you can recognise when it fits, and so you can confidently say "not yet" when it does not.
Sources
Before you leave — how confident are you with this?
Your honest rating shapes when you'll see this again. No grades, no shame.
Comments
Loading comments…