Architecture micro-frontends scalable avec Module Federation : host/remote apps, partage de dépendances, routing fédéré et bonnes pratiques.
Architecture micro-frontend — pourquoi ?
Les micro-frontends appliquent les principes des microservices au frontend : chaque équipe développe, teste et déploie son périmètre de façon totalement autonome. L'application finale est une composition de ces périmètres indépendants.
| Problème monorepo/monolithique | Solution micro-frontend |
|---|---|
| Build de 15 min pour un changement dans un module | Build en 2 min pour le module concerné uniquement |
| Conflit de merge entre 10 équipes sur le même repo | Chaque équipe dans son repo, zéro conflit |
| Déploiement de toute l'app pour corriger un bug mineur | Déploiement ciblé du seul micro-frontend affecté |
| Migration Angular impossible sans arrêter tout | Migration progressive micro-frontend par micro-frontend |
Module Federation : concept Host-Remote
Webpack Module Federation (Webpack 5) permet à une application de charger des modules JavaScript d'une autre application au runtime, partageant les dépendances communes pour éviter les doublons.
| Rôle | Responsabilité | Fichier clé |
|---|---|---|
| Host | Application principale, orchestre le chargement des remotes | remoteEntry.js consommé depuis les remotes |
| Remote | Application indépendante exposant des modules | remoteEntry.js servi par le remote |
| Shared | Dépendances partagées (Angular, RxJS) | Négociation automatique à la résolution |
Installation avec @angular-architects
# Créer la host app
ng new shell-app --routing --style=scss
cd shell-app
ng add @angular-architects/module-federation --project shell-app --port 4200 --type host
# Créer les remote apps (dans des repos séparés ou un monorepo)
ng new user-remote --routing --style=scss
cd user-remote
ng add @angular-architects/module-federation --project user-remote --port 4201 --type remote
ng new product-remote --routing --style=scss
cd product-remote
ng add @angular-architects/module-federation --project product-remote --port 4202 --type remote
ng add @angular-architects/module-federation génère le webpack.config.js, met à jour angular.json avec le builder custom, et crée les fichiers de configuration nécessaires.
Configuration Host App complète
// webpack.config.js (Host App — shell-app)
import { withModuleFederationPlugin } from '@angular-architects/module-federation/webpack';
export default withModuleFederationPlugin({
name: 'shell',
remotes: {
// Alias utilisé dans le code → URL du remoteEntry.js
// En production, remplacer localhost par les URLs réelles
'userRemote': 'user_remote@http://localhost:4201/remoteEntry.js',
'productRemote': 'product_remote@http://localhost:4202/remoteEntry.js',
'authRemote': 'auth_remote@http://localhost:4203/remoteEntry.js',
},
shared: {
'@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/common': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/forms': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'rxjs': { singleton: true, strictVersion: false, requiredVersion: 'auto' },
},
});
// Charger les URLs de remotes depuis une config dynamique (production)
// module-federation.manifest.json
{
"userRemote": "https://users.myapp.com/remoteEntry.js",
"productRemote": "https://products.myapp.com/remoteEntry.js",
"authRemote": "https://auth.myapp.com/remoteEntry.js"
}
// main.ts — Charger le manifest avant le bootstrap
import { loadManifest } from '@angular-architects/module-federation';
loadManifest('/assets/module-federation.manifest.json')
.then(() => import('./bootstrap'))
.catch(err => console.error('MF manifest error:', err));
Configuration Remote App
// webpack.config.js (Remote App — user-remote)
import { withModuleFederationPlugin } from '@angular-architects/module-federation/webpack';
export default withModuleFederationPlugin({
name: 'user_remote',
exposes: {
// './CheminExposé': './chemin/vers/fichier.ts'
'./UserModule': './src/app/user/user.module.ts',
'./UserComponent': './src/app/user/user.component.ts',
},
shared: {
'@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/common': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'rxjs': { singleton: true, strictVersion: false, requiredVersion: 'auto' },
},
});
// user.module.ts — Module exposé avec son propre routage
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { UserListComponent } from './user-list/user-list.component';
import { UserDetailComponent } from './user-detail/user-detail.component';
const routes: Routes = [
{ path: '', component: UserListComponent },
{ path: ':id', component: UserDetailComponent },
];
@NgModule({
declarations: [UserListComponent, UserDetailComponent],
imports: [RouterModule.forChild(routes)], // forChild — pas forRoot
})
export class UserModule {}
Partage des dépendances
La négociation des versions partagées est automatique avec requiredVersion: 'auto'. Voici les stratégies pour les cas complexes :
shared: {
'@angular/core': {
singleton: true, // Une seule instance → évite les conflits d'injection
strictVersion: true, // Lève une erreur si versions incompatibles
requiredVersion: 'auto', // Lit depuis package.json automatiquement
eager: false, // Chargé à la demande (recommandé)
},
'rxjs': {
singleton: true,
strictVersion: false, // Tolère les patches différents (15.x compatible 15.y)
requiredVersion: 'auto',
},
// Librairie propre partagée entre remotes
'@mycompany/design-system': {
singleton: true,
strictVersion: false,
// Pas de requiredVersion → chaque remote peut avoir une version différente
// mais une seule sera chargée (risque de breaking changes)
},
}
singleton: true, strictVersion: true — deux instances Angular dans le même navigateur = crash garanti (providers dupliqués, services isolés, injection qui échoue). RxJS tolère mieux les versions mineures différentes.
Routage et chargement dynamique
// app.routes.ts (Host — shell-app)
import { loadRemoteModule } from '@angular-architects/module-federation';
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', loadComponent: () => import('./dashboard/dashboard.component') },
// Chargement d'un module entier depuis un remote
{
path: 'users',
loadChildren: () =>
loadRemoteModule({
type: 'manifest', // Lit depuis module-federation.manifest.json
remoteName: 'userRemote',
exposedModule: './UserModule',
}).then(m => m.UserModule),
},
// Chargement d'un composant standalone depuis un remote
{
path: 'products/:id',
loadComponent: () =>
loadRemoteModule({
type: 'manifest',
remoteName: 'productRemote',
exposedModule: './ProductDetailComponent',
}).then(m => m.ProductDetailComponent),
},
// Fallback si remote indisponible
{ path: '**', redirectTo: 'dashboard' },
];
Communication entre micro-frontends
Les micro-frontends ne doivent pas se coupler directement. Les patterns de communication recommandés :
// Pattern 1 : Service partagé dans la Host (singleton)
// shared-state.service.ts (dans la host, partagé via DI)
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' }) // Singleton dans tout le navigateur
export class SharedStateService {
readonly currentUser = signal<User | null>(null);
readonly cart = signal<CartItem[]>([]);
setUser(user: User) { this.currentUser.set(user); }
addToCart(item: CartItem) {
this.cart.update(items => [...items, item]);
}
}
// Dans un remote (user-remote), injecter le service partagé
@Component({ ... })
export class UserProfileComponent {
private sharedState = inject(SharedStateService); // Même instance que la host
login(user: User) {
this.sharedState.setUser(user); // Notifie tous les micro-frontends
}
}
// Pattern 2 : Custom Events (découplage total)
// remote → host via CustomEvent
// Dans le remote : émettre un événement
document.dispatchEvent(new CustomEvent('mfe:user:login', {
detail: { userId: 42, name: 'John' },
bubbles: true,
}));
// Dans la host : écouter
window.addEventListener('mfe:user:login', (event: CustomEvent) => {
console.log('User logged in:', event.detail);
this.authService.setCurrentUser(event.detail);
});
Approche Nx Monorepo
Nx est la solution recommandée pour gérer plusieurs micro-frontends dans un monorepo. Il intègre nativement Module Federation avec une configuration simplifiée.
# Créer un workspace Nx avec Module Federation
npx create-nx-workspace@latest my-mfe --preset=apps
# Ajouter les applications
nx generate @nx/angular:host shell --port=4200
nx generate @nx/angular:remote user-dashboard --host=shell --port=4201
nx generate @nx/angular:remote product-catalog --host=shell --port=4202
# Lancer tous les microfrontends en parallèle
nx run-many --target=serve --all
# Build ciblé (Nx cache les builds qui n'ont pas changé)
nx build shell # Build toute la composition
nx build user-dashboard # Build uniquement ce remote
user-dashboard n'a pas changé, son build est servi depuis le cache en quelques secondes. Gain de temps énorme dans les grandes équipes.
Conclusion
Module Federation avec Angular est mature et production-ready en 2026. Les points critiques à maîtriser :
- Utiliser
@angular-architects/module-federation— simplifie la configuration Webpack - Manifest JSON dynamique pour les URLs de remotes en production
singleton: true, strictVersion: truepour Angular — obligatoire- Services partagés via la host (DI) ou CustomEvents pour la communication
- Nx Monorepo pour les équipes multiples — cache distribué et build incrémental
- Documenter le contrat de chaque remote (modules exposés, versions, API publique)