Building Micro Frontends with Angular and Module Federation
Module Federation is a Webpack 5 feature that lets multiple JavaScript applications share code at runtime. Instead of bundling everything at build time, applications can load modules from other applications while they’re running.
How It Works
Module Federation uses two concepts: hosts and remotes. A host application loads modules from remote applications. The remote applications expose specific modules that hosts can import. The key difference from normal imports is that this happens at runtime, not during the build.
This means you can deploy the remote applications independently. When a remote updates, the host picks up the changes without needing a rebuild.
Architecture
My demo project uses three Angular applications:
- Shell: The host application that loads the remotes
- MFE1: A remote that exposes a module
- MFE2: Another remote that exposes a different module
The shell treats the remote modules as lazy-loaded routes in Angular’s router.
Configuration
The remote applications need a webpack config that specifies what to expose:
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "mfe1",
filename: "remoteEntry.js",
exposes: {
'./Module': './src/app/remote-entry/entry.module.ts',
},
shared: {
"@angular/core": { singleton: true, strictVersion: true },
"@angular/common": { singleton: true, strictVersion: true },
"@angular/router": { singleton: true, strictVersion: true },
}
})
]
};
The exposes section defines what other applications can import. The shared section lists dependencies that should only be loaded once. With singleton: true, Angular core gets loaded once and shared across all applications instead of each remote bundling its own copy.
Loading Remotes in Angular
The host application loads remotes using Angular’s lazy loading:
const routes: Routes = [
{
path: 'mfe1',
loadChildren: () =>
import('mfe1/Module').then(m => m.RemoteEntryModule)
}
];
TypeScript doesn’t know about these dynamic module names, so you need to declare them:
declare module 'mfe1/Module';
declare module 'mfe2/Module';
This tells TypeScript the modules exist without providing full type information.
Problems to Consider
Version compatibility: All applications need compatible Angular versions. If MFE1 uses Angular 15 and MFE2 uses Angular 14, you’ll run into issues with the shared singleton dependencies.
State management: If remotes need to share state, you need a plan for that. Module Federation doesn’t solve state sharing automatically.
Error handling: Remote modules can fail to load due to network issues or deployment problems. The host needs to handle these failures gracefully.
Build setup: Each application needs its own webpack config. This adds complexity compared to a standard Angular CLI setup.
When This Makes Sense
Module Federation is useful when you have multiple teams working on different parts of a large application and need to deploy independently. It’s also useful for building plugin architectures where the core application loads optional modules at runtime.
For smaller applications or teams, the added complexity usually isn’t worth it. Standard lazy loading and code splitting work fine in those cases.
Results
The demo shows the basic setup. Each remote runs independently at different ports (4201, 4202), and the shell (4200) loads them at runtime. You can update MFE1, redeploy it, and the shell picks up the changes without any rebuild.
The full implementation is available at github.com/peteqian/demo-angular-mod-fed.