Micro frontends with Module Federation and Webpack 5
Nicolás Delfino / October 19, 2020
7 min read
Micro Frontends with Module Federation and Webpack 5

The release of Webpack 5 delivered something special besides performance improvements like improved tree-shaking and persistent caching across builds - an architectural possibility called Module Federation.
Module Federation (MF) enables applications to seamlessly consume and expose code in a way that hasn’t been possible before, paving the way for micro frontend composition in JS land and general federation of whatever you choose to pass through Webpack.
In this post we'll take a look into the world of Module Federation with Webpack 5 and how we can utilize this tool to create a foundation for multiple teams working on the same product.
Federated E-Commerce Application#
To demonstrate how Module Federation works, lets get started tackling our simplified e-commerce scenario once again, but this time from React SPA land.
The final application consists of a landing page with a bunch of products, a minicart and a checkout page. What's special about it is that each page is a standalone, isolated micro frontend.
Content#
Monorepo / Yarn Workspaces#
Although not a requirement from a Module Federation point of view, we went the monorepo route along with Yarn workspaces to facilitate running all of the separate applications simultaneously.
"private": true,
"scripts": {
"installDependencies": "yarn workspaces run deps",
"build": "yarn workspaces run build",
"start": "concurrently \"wsrun --parallel start\"",
"clean": "rm -fr node_modules sites/**/node_modules && yarn run clean:dist",
"clean:dist": "rm -fr node_modules sites/**/dist"
},
"workspaces": [
"sites/*"
],
App Shell#
The app shell is our main SPA, contains the routes, the Redux store and is the host for all of our remote applications.
import 'team-shell/BaseStyles';
import store from 'team-shell/Store';
const Shell = () => (
<Provider store={store}>
<Router />
</Provider>
);
const Landing = React.lazy(() => import('team-landing/Landing'));
const Checkout = React.lazy(() => import('team-checkout/Checkout'));
const Cart = React.lazy(() => import('team-checkout/Cart'));
const LandingRoute = () => (
<React.Suspense fallback={<div className="fd_error_landing" />}>
<Landing />
</React.Suspense>
);
const CheckoutRoute = () => (
<React.Suspense fallback={<div className="fd_error_checkout" />}>
<Checkout />
</React.Suspense>
);
const ShoppingCart = () => (
<React.Suspense fallback={<div className="fd_error_cart" />}>
<Cart />
</React.Suspense>
);
const Routes = () => {
return (
<Router>
<nav>
<NavLinks />
<div>
<ShoppingCart />
</div>
</nav>
<Switch>
<Route path="/" exact>
<LandingRoute />
</Route>
<Route path="/checkout">
<CheckoutRoute />
</Route>
</Switch>
</Router>
);
};
const reducer = (state = { items: [] }, { type, payload }) =>
produce(state, (draft) => {
switch (type) {
case 'cart/add': {
draft.items.push(payload);
return draft;
}
case 'cart/delete': {
const { id } = payload;
draft.items.splice(id, 1);
return draft;
}
default: {
return draft;
}
}
});
new ModuleFederationPlugin({
name: "shell",
filename: "remoteEntry.js",
remotes: {
"team-shell": "shell@http://localhost:3000/remoteEntry.js",
"team-landing": "landing@http://localhost:3001/remoteEntry.js",
"team-checkout": "checkout@http://localhost:3002/remoteEntry.js",
},
exposes: {
"./Store": "./src/federated/store",
"./BaseStyles": "./src/styles/federated/base.css"
},
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
"react-dom": {
singleton: true,
requiredVersion: deps["react-dom"],
},
},
}),
- Exposes and consumes the Redux store
- Exposes base styles
- Handles routes
Team Checkout#
The checkout team exposes the checkout page route, the buy button and the cart:
name: "checkout",
filename: "remoteEntry.js",
remotes: {
"team-shell": "shell@http://localhost:3000/remoteEntry.js",
"team-landing": "landing@http://localhost:3001/remoteEntry.js"
},
exposes: {
"./Checkout": "./src/federated/Checkout",
"./BuyButton": "./src/federated/BuyButton",
"./Cart": "./src/federated/Cart",
},
const BuyButton = ({ payload, addToCart, children }) => (
<button onClick={() => addToCart(payload)}>{children}</button>
);
export default connect(null, (dispatch) => ({
addToCart: (payload) => dispatch({ type: 'cart/add', payload })
}))(BuyButton);
const Checkout = ({products}) => {
return (
(...map products)
)
};
const mapStateToPros = state => ({
items: state.items
})
export default connect(mapStateToPros)(Checkout);
Checkout - Standalone#
Besides its setup to share and consume UI, checkout also exposes itself as a standalone application. This setup makes it easy for the team to develop their application and also enables them to create catalogs of the Micro frontends they own and provide to the outside world.
import { products } from 'team-landing/MockedProducts';
const Standalone = () => (
<>
<h2>Checkout (standalone)</h2>...map products
</>
);

- Exposes the checkout route
- Buy button
- Cart
- Standalone
Team Landing#

name: "landing",
filename: "remoteEntry.js",
remotes: {
"team-shell": "shell@http://localhost:3000/remoteEntry.js",
"team-landing": "landing@http://localhost:3001/remoteEntry.js",
"team-checkout": "checkout@http://localhost:3002/remoteEntry.js",
},
exposes: {
"./Landing": "./src/federated/Landing",
"./MockedProducts": "./src/federated/mocks/products",
},
import BuyButton from "team-checkout/BuyButton"
products.map((product, index) => {
return (
<div className="product" key={...}>
<div>{item.name}</div>
<BuyButton payload={...product}>BUY - ${item.price}</BuyButton>
</div>
)
})
- Exposes the landing (product) route and mocked product data
- Uses buy button from team Checkout
- Standalone
Dependencies#
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
"react-dom": {
singleton: true,
requiredVersion: deps["react-dom"],
},
},
This is telling Webpack - see all of my runtime dependencies specified inside package.json as shared dependencies against others (deps), but treat libraries like React and react-dom (libraries that don't allow multiple instantiations) - as singletons. Ensuring that Webpack only loads these libraries once.
Summary#
Module federation gives us the opportunity to have multiple teams output small subsets of a site and consume them at runtime within Single Page Applications, to share UI components without NPM, to consume complete configurations, business logic etc, anything you can run through Webpack is now shareable.
The nature of MF also makes it possible to federate parts of an existing application one feature at a time, offloading responsibilities in a team manner instead of continuously adding complexity to a monolithic app.
Resources#
Source code for this post is as always available on my Github
Simplified SSR example at Module Federation Examples
Concepts & inspiration for the examples of this post comes from the only book out there today about Module Federation "The Practical Guide to Module Federation" by Jack Herrington & Zach Jackson (Zach is the creator of Module Federation). It's really well written and full with information that'll surely help you going forward with Module Federation.
/ ND