AngularAtlaséd. 22.2

Chap. 13Architecture à grande échelle57 / 75

Micro frontends

Quand découper une application en plusieurs déploiements vaut le coup, et quand c’est juste de la complexité : Native Federation, Module Federation, iframes, Web Components.

architecture ~0 min de lecture

Un micro frontend est un morceau d’UI construit, testé et livré indépendamment par une équipe, puis composé avec les autres à l’exécution, dans le navigateur. Le mot qui compte est « indépendamment » : si deux parties doivent être livrées ensemble, ce ne sont pas des micro frontends mais des modules d’une même application — aussi séparés que soient leurs dossiers.

Cette architecture résout un problème organisationnel, pas technique : plusieurs équipes autonomes qui veulent livrer à des rythmes différents sans se bloquer mutuellement. Le prix se paie en complexité à l’exécution : versions des dépendances, échecs d’intégration, cohérence visuelle, tests, observabilité.

En bref
  • Les micro frontends résolvent un problème d’organisation (des équipes qui livrent indépendamment), pas un problème technique.
  • Le prix est réel : plus de JavaScript, des versions de dépendances à garder alignées, des tests end-to-end plus difficiles.
  • Pour Angular avec esbuild, l’option recommandée est Native Federation. Web Components et iframes conviennent quand on mélange des frameworks ou qu’on veut une isolation totale.
  • Les contrats entre applications (routes, événements, versions) comptent plus que la technologie. Chaque application chargée à distance a besoin d’un fallback.
  • Avec une seule équipe, un monorepo aux frontières claires est presque toujours plus simple.
Pourquoi c’est important

La plupart des applications Angular, même grandes, sont mieux servies par une seule application dans un monorepo, avec des frontières claires et du lazy loading. Les micro frontends sont la bonne réponse à quelques situations précises, et une source coûteuse de problèmes sinon. Cette page vous aide à les distinguer et, si besoin, à les construire correctement avec Native Federation.

Ce que c’est et ce que ce n’est pas

Un système de micro frontends a trois propriétés, toutes obligatoires :

  • Build séparé — chaque partie a son propre pipeline et son propre package.json (ou au moins sa propre cible de build).
  • Déploiement séparé — l’équipe Catalog publie une nouvelle version sans que le shell ou l’équipe Cart n’ait à intervenir.
  • Composition à l’exécution — un shell (host) ne découvre que dans le navigateur quelle version de chaque remote charger, généralement à partir d’un manifest.
BUILD — PAS un micro frontend
  libs/catalog ─┐
  libs/cart    ─┼─► ng build shell ─► un seul déploiement
  libs/reviews ─┘
  une version, une release, des contrats vérifiés par le compilateur

EXÉCUTION — micro frontends
  catalog → build → deploy → catalog…/remoteEntry.json ─┐
  cart    → build → deploy → cart…/remoteEntry.json    ─┼─► le shell les compose
  reviews → build → deploy → reviews…/remoteEntry.json ─┘   dans le navigateur
  N versions, N releases, contrats vérifiés seulement à l’exécution
CompositionComment ça marcheEst-ce un micro frontend ?
Bibliothèques dans un monorepole shell importe libs/catalog ; un seul build, un seul déploiementnon — c’est au build (voir Monorepo)
Lazy routes, @deferdes chunks séparés, mais produits par le même buildnon — la même application
Native / Module Federationles remotes publient des modules ES + des métadonnées ; le shell les charge depuis le manifest et partage les dépendancesoui
Web Componentschaque équipe livre un bundle qui définit des balises ; le shell ne fait que les utiliseroui (voir Angular Elements)
iframesdes documents séparés, dans un frameoui — l’isolation la plus forte

Vraies raisons, fausses raisons

Les raisons qui justifient le prix concernent presque toutes les personnes et la livraison :

  • Des équipes autonomes avec des cadences différentes. L’équipe checkout livre plusieurs fois par jour, celle du catalogue une fois par semaine, et une release commune bloque les deux : une régression dans le catalogue arrête un correctif de paiement urgent.
  • Les très grandes organisations. Des dizaines de développeurs frontend sur le même produit, plusieurs domaines métier, chacun avec une responsabilité claire (loi de Conway : l’architecture finit de toute façon par refléter l’organisation).
  • La migration progressive (strangler fig). Un nouveau shell « enveloppe » l’ancienne application (AngularJS, un Angular plus ancien, un autre framework) ; les écrans migrent un par un, et le vieux code rétrécit jusqu’à disparaître — sans réécriture « big bang ».
  • Des produits séparés sous une seule expérience : acquisitions, unités métier, portails qui regroupent des applications existantes.

Les fausses raisons sonnent bien en présentation, mais ont des solutions moins chères :

« On veut des MFE pour… »Pourquoi ça ne tient pasCe qui le résout vraiment
scalabilité / performanceles MFE ajoutent des octets, des requêtes et une négociation de versions au démarragelazy routes, @defer, SSR, optimisations
un code propre et modulairela modularité est une propriété du code, pas du déploiementbibliothèques + règles d’import (frontières, structure)
des builds plus rapidesN pipelines, ce sont N endroits qui peuvent casserbuild incrémental et cache (Nx affected) dans un monorepo
chaque équipe choisit son propre frameworkla diversité technologique est un coût permanent : un design system dupliqué, des personnes difficiles à faire passer d’une équipe à l’autreà n’accepter que lorsque c’est inévitable (migration, acquisition)
Le test simple

Si vous ne pouvez pas nommer deux équipes qui se bloquent aujourd’hui au moment de la release, vous n’avez pas le problème que résolvent les micro frontends.

Le prix

Chacun des coûts ci-dessous apparaît en pratique ; la différence entre un système sain et un système fragile, c’est d’avoir un plan dès le premier jour.

CoûtCe qui se passe concrètementComment le maîtriser
Versions partagéesun remote construit avec un autre Angular ne peut pas utiliser le runtime du shell ; les montées de version majeure doivent être coordonnéesla même version partout, un calendrier de montées de version commun, une vérification en CI
Intégration à l’exécutionun export renommé dans un remote casse le shell en production, pas à la compilationexposes traité comme une API publique (semver), des tests de contrat, un fallback par remote
Bundles dupliquéstout ce qui n’est pas partagé est téléchargé N fois ; tout ce qui est partagé est construit séparément, donc n’est plus tree-shaké par applicationun analyseur de bundle, ne partager que ce qui a du sens
Design systemcinq équipes, cinq versions des boutonsle design system comme paquet partagé + tokens CSS, géré par une équipe plateforme
Routingqui possède l’URL, que se passe-t-il sur un lien direct vers un remotele shell possède les routes de premier niveau, chaque remote exporte des Routes pour son préfixe
Auth et sessionle token doit atteindre tous les remotes, le refresh ne doit avoir lieu qu’une foisun cookie HttpOnly ou un Service d’auth venant d’une bibliothèque partagée, en singleton (refresh d’auth)
Tests E2Echaque équipe est au vert isolément, le bug apparaît dans la compositione2e sur « shell + les versions de production des autres »
Observabilité« erreur en production » — de quel remote, de quelle version ?la version de chaque remote dans chaque log, des source maps par remote
Développement localvous démarrez cinq serveurs pour travailler sur un seulun manifest par environnement : le shell local charge les remotes déployés, vous ne lancez que ce que vous modifiez

Native Federation pas à pas

Native Federation implémente le modèle Module Federation sur des standards du web : les remotes sont de simples modules ES, reliés par une import map. Pour Angular 22, le paquet est @angular-architects/native-federation 22.x — la version suit le major.minor d’Angular — et il reste sur le builder esbuild du CLI (@angular/build:application), qu’il enveloppe. Le projet a déménagé vers l’organisation github.com/native-federation. Le mécanisme, pas à pas :

  1. Build. Chaque remote produit des modules ES et un remoteEntry.json : ce qu’il expose (./routes → le fichier généré) et les dépendances qu’il partage, avec version, requiredVersion, singleton, strictVersion.
  2. Démarrage du shell. main.ts appelle initFederation() avant Angular : il lit le manifest (nom → URL), télécharge les métadonnées de chaque remote, négocie les versions et écrit l’import map (avec es-module-shims).
  3. Bootstrap. Seulement ensuite, import('./bootstrap') démarre Angular. C’est pourquoi le schematic déplace le bootstrap dans un fichier séparé : @angular/core doit être résolu via l’import map, il faut donc qu’elle existe d’abord.
  4. Navigation. loadRemoteModule('catalog', './routes') est, en substance, un import() dynamique d’une URL sur le serveur de l’équipe Catalog. Quand le code du remote fait import '@angular/core', l’import map l’envoie vers la copie du shell : un seul Angular, un seul injector racine.
# la version de l’adaptateur suit le major.minor d’Angular (22.2.x pour Angular 22.2)
npm i -D @angular-architects/native-federation

ng g @angular-architects/native-federation:init \
  --project catalog --port 4201 --type remote
ng g @angular-architects/native-federation:init \
  --project shell --port 4200 --type dynamic-host

# Ce que change le schematic :
#  • build/serve → @angular-architects/native-federation:build,
#    qui délègue à @angular/build:application (esbuild)
#  • main.ts → initFederation(...) puis import('./bootstrap');
#    l’ancien bootstrap se retrouve dans bootstrap.ts
#  • génère federation.config.mjs (+ public/federation.manifest.json sur dynamic-host)
#  • ajoute es-module-shims aux polyfills (import maps avec scopes)
#  • avec --webcomponent : bootstrap sous forme de custom element (createApplication + define)
// catalog/federation.config.mjs — un remote, tel que généré par la 22.x
import {
  withNativeFederation,
  fromPackageJson,
} from '@angular-architects/native-federation/config';

export default withNativeFederation({
  name: 'catalog',

  exposes: {
    // l’API publique du remote
    './routes': './projects/catalog/src/app/catalog.routes.ts',
  },

  shared: fromPackageJson({
    singleton: true,
    strictVersion: true,
    requiredVersion: 'auto',
    build: 'package',
  })
    // tout @angular/core, entry points secondaires compris → une seule copie
    .patch(['@angular/core'], { includeSecondaries: { keepAll: true } }),

  skip: ['rxjs/ajax', 'rxjs/fetch', 'rxjs/testing', 'rxjs/webSocket'],

  features: { denseChunking: true },
});
// shell/src/main.ts — s’exécute AVANT Angular :
// lit le manifest, télécharge les fichiers remoteEntry.json, négocie les versions,
// écrit l’import map — et seulement ensuite charge bootstrap.ts (et @angular/core)
import { initFederation } from '@angular-architects/native-federation';

initFederation('federation.manifest.json')
  .then((nf) => import('./bootstrap').then((m) => m.bootstrap(nf)))
  .catch((err) => console.error(err));
Ce qui a changé en 22.x
  • Pour Angular 22, Native Federation v4 revient dans le paquet principal @angular-architects/native-federation (pour Angular 20–21, il y avait le pont -v4).
  • Le schematic génère federation.config.mjs avec fromPackageJson(...) ; shareAll et share restent disponibles.
  • loadRemoteModule importé directement depuis le paquet est marqué @deprecated : utilisez celui du résultat de initFederation(), comme dans app.routes.ts.
// La forme courte des anciens tutoriels existe encore en 22.x,
// mais elle est marquée @deprecated :
import { loadRemoteModule } from '@angular-architects/native-federation';

const route = {
  path: 'catalog',
  loadChildren: () =>
    loadRemoteModule('catalog', './routes').then((m) => m.CATALOG_ROUTES),
};

// La raison, d’après le d.ts : elle dépend d’une instance globale et du DERNIER initFederation,
// elle est donc fragile dans les tests et avec plusieurs hosts.
// Recommandé : loadRemoteModule issu du résultat d’initFederation (voir app.routes.ts).

L’exemple ci-dessous reproduit exactement ce flux. Les serveurs sont simulés, mais chaque « remote » est un vrai composant Angular, placé dans son propre chunk et chargé avec un import() dynamique — le même mécanisme que loadRemoteModule utilise en dessous. Le catalogue et le panier ne communiquent que par des événements sur window ; les avis ne parlent à personne.

LiveShell + trois remotes (simulation, avec un vrai lazy loading)
federation.manifest.json (modifiable)
Déploiements (serveurs simulés)
catalog.atlas.example
cart.atlas.example
reviews.atlas.example
shell@angular/core 22.2.2panier: —
<slot catalog>idle
<slot cart>idle
<slot reviews>idle
Ce que fait le shell
Mettez reviews « hors ligne », mettez cart sur @angular/core 21.2.4 ou cassez une URL dans le manifest, puis appuyez sur « Charger la page » : chaque slot échoue séparément, avec un fallback et « Réessayer ». « lent (4 s) » dépasse le timeout du shell. Ajoutez depuis le catalogue : le badge du shell se met à jour via un événement. Dans DevTools → Network, vous voyez les vrais chunks des remotes.

Dépendances partagées et versions

Pour chaque paquet de shared, un remote déclare quatre choses : version (avec quoi il a été construit), requiredVersion (ce qu’il accepte ; 'auto' = la plage de package.json, par exemple ^22.2.0), singleton (une seule instance pour tous) et strictVersion (refuse une version hors de la plage).

Pourquoi Angular doit être un singleton. @angular/core n’est pas une bibliothèque de fonctions pures : c’est le runtime qui fait le rendu, l’injector, la Change Detection. Les templates compilés appellent les instructions du runtime du build avec lequel ils ont été compilés, donc un composant d’une copie ne peut pas être rendu par une autre. Même pour une bibliothèque ordinaire, deux copies donnent deux classes différentes : inject(AuthService) dans le remote demande un autre token que celui enregistré par le shell, et votre « singleton » a soudain deux instances.

Ce qui se passe quand les versions ne correspondent pas dépend de l’outil :

Situation (singleton: true)Native Federation (orchestrator v4)Module Federation (webpack)
la version partagée satisfait requiredVersionle remote utilise la copie partagée ; la version du host a prioritéutilise le singleton
ne satisfait pas, strictVersion: truele remote obtient sa propre copie, dans les scopes de l’import maperreur au chargement : Unsatisfied version … of shared singleton module …
ne satisfait pas, strictVersion: falseutilise quand même la version partagée + un avertissementutilise le singleton + console.warn

Pour Angular, « sa propre copie » ne sauve pas la situation : le remote aurait un autre runtime, donc ses composants ne peuvent pas entrer dans l’arbre du shell. Un remote sur une autre version majeure ne s’intègre qu’en isolation — comme Web Component avec son propre bootstrap, ou dans un iframe. Si vous préférez le découvrir au démarrage plutôt qu’à la navigation, l’orchestrator a l’option strict.strictExternalCompatibility, qui lève une erreur à l’initialisation au lieu de créer des scopes.

// Trois façons d’écrire `shared` dans federation.config.mjs

// 1. le défaut en 22.x : tout ce qui est dans dependencies de package.json, mêmes règles
shared: fromPackageJson({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),

// 2. le style classique (toujours exporté)
shared: { ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }) },

// 3. explicite, paquet par paquet
shared: share({
  '@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
  // a de l’état → singleton
  '@ngrx/signals': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
  // utilitaire sans état : chaque remote peut avoir sa propre copie
  'date-fns': { singleton: false },
}),

// requiredVersion: 'auto' → la plage de package.json (p. ex. "^22.2.0")
// autoShareScope({ level: 'minor' }) → ne partage qu’entre remotes
//   de la même ligne de versions (p. ex. "ng22.2")
LiveNégociation des versions et import map (simulation)

Le shell (host) fournit @angular/core en version 22.2.2, qui a priorité dans le scope global.

Remoteconstruit avecrequiredVersionsingletonstrictVersion
catalog
cart
reviews
catalogshare
22.2.2 satisfait ^22.2.2 → utilise la copie du shell, ne télécharge rien de plus.
cartscope
22.2.2 ne satisfait pas ~22.1.4 ; avec strictVersion, il obtient sa propre copie 22.1.4, visible seulement dans son scope. Deux runtimes Angular : ses composants ne peuvent pas être rendus dans l’arbre du shell — intégrez-le en isolation (Web Component avec son propre bootstrap).
reviewsskip + avertissement
22.2.2 ne satisfait pas ^21.2.4, mais strictVersion: false → utilise quand même 22.2.2, avec un avertissement dans la console. Cela peut fonctionner, ou planter à l’exécution — vous le découvrirez en production.
Copies de @angular/core téléchargées : 2un runtime Angular supplémentaire
import map générée
{
  "imports": {
    "@angular/core": "https://shell.example/core-22.2.2.js"
  },
  "scopes": {
    "https://cart.example/": {
      "@angular/core": "https://cart.example/core-22.1.4.js"
    }
  }
}
Simule les règles du resolver de Native Federation pour @angular/core. Changez la version, la plage et les flags de chaque remote, et suivez le verdict et l’import map. Remarquez comment ~ (n’accepte que les patchs) crée une copie supplémentaire sans vraie raison, et comment singleton: false crée toujours un second Angular.
  • Idéal : exactement la même version d’Angular dans le shell et dans tous les remotes. ^ donne de la tolérance entre mineures, mais le code compilé en AOT utilise des instructions internes du runtime — testez la combinaison, ne vous fiez pas à semver.
  • Le host gagne : à chaque nouvelle mineure, le shell peut donc passer en premier, tant que les plages des remotes l’acceptent. Une nouvelle majeure exige une montée de version synchronisée, ou d’isoler le remote en retard.
  • Singleton + strictVersion pour le framework et tout ce qui a de l’état : Angular, router, stores, design system, bibliothèque d’auth. Les utilitaires sans état peuvent rester non-singleton.
  • autoShareScope() lie le partage à une ligne de versions (par exemple ng22.2), pour ne pas mélanger accidentellement des builds incompatibles.

Module Federation, Web Components, iframes

Module Federation (webpack)

@angular-architects/module-federation a une version pour Angular 22, mais c’est la variante « classique », sur webpack : ng add fait passer le projet du builder esbuild à @angular-builders/custom-webpack (à partir d’Angular 22 ; jusqu’à la 21 il utilisait ngx-build-plus), et il vous avertit explicitement que, pour les nouveaux projets, l’alternative est Native Federation. Il existe aussi un builder rspack, marqué expérimental. Ne le choisissez que si vous étendez un système Module Federation existant ou devez partager le runtime avec des remotes déjà construits sur webpack.

// Module Federation classique (webpack) — @angular-architects/module-federation 22.x
const {
  shareAll,
  withModuleFederationPlugin,
} = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({
  name: 'catalog',
  exposes: {
    './routes': './projects/catalog/src/app/catalog.routes.ts',
  },
  shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
  },
});

Web Components

Chaque remote démarre sa propre application avec createApplication() et définit une balise ; le shell se contente de la poser sur la page. Cela fonctionne entre versions majeures d’Angular différentes et entre frameworks, car rien n’est partagé. C’est exactement son coût : chaque bundle apporte son propre runtime (≈ 33 kB transférés pour un élément simple, mesurés sur la page Angular Elements), il n’y a pas de DI commune, et la communication passe par des attributs et des événements. Cela se combine bien avec la federation : un remote sur une autre majeure est exposé comme Web Component et chargé quand même via loadRemoteModule.

// Un remote sur une AUTRE version majeure d’Angular (ou un autre framework) : intégré en isolation,
// comme Web Component avec son propre runtime.
//
// remote — federation.config.mjs :  exposes: { './Bootstrap': './src/bootstrap.ts' }
// remote — bootstrap.ts :
//   createApplication(appConfig).then(({ injector }) =>
//     customElements.define('mfe-legacy', createCustomElement(App, { injector })));
//
// shell — une page qui charge le bundle (son effet : define), puis utilise la balise
import { CUSTOM_ELEMENTS_SCHEMA, Component, inject, input, resource } from '@angular/core';
import { NATIVE_FEDERATION } from '../app.config';

@Component({
  selector: 'app-legacy-page',
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    @if (bundle.hasValue()) {
      <mfe-legacy [attr.customer-id]="customerId()"></mfe-legacy>
    } @else if (bundle.error()) {
      <p role="alert">
        Le module legacy n’est pas disponible.
        <button (click)="bundle.reload()">Réessayer</button>
      </p>
    }
  `,
})
export class LegacyPage {
  private readonly nf = inject(NATIVE_FEDERATION);
  readonly customerId = input.required<string>();

  protected readonly bundle = resource({
    loader: () => this.nf.loadRemoteModule('legacy', './Bootstrap'),
  });
}

iframes

L’isolation la plus forte : un autre document, un autre JavaScript, un autre CSS ; les erreurs, les fuites mémoire et les styles restent dans le frame, et avec sandbox vous pouvez exécuter du code auquel vous ne faites pas confiance. Vous la payez en intégration : la hauteur se synchronise via postMessage, un dialog ne peut pas sortir du frame, les deep links et l’historique du navigateur doivent être reliés à la main, le focus et l’accessibilité se compliquent, et chaque frame charge toute l’application. Adapté au code tiers, aux applications legacy que vous ne pouvez pas modifier et aux outils internes.

<!-- L’isolation la plus forte : un autre document, un autre JS, un autre CSS ;
     les erreurs restent dans le frame -->
<iframe
  src="https://billing.exemplu.ro/embed?customer=42"
  title="Facturation"
  sandbox="allow-scripts allow-same-origin allow-forms"
  loading="lazy"
  style="width: 100%; border: 0"
></iframe>

<!-- Le prix : la hauteur se communique via postMessage, un dialog ne peut pas sortir
     du frame, les deep links et l’historique du navigateur se synchronisent à la main,
     et chaque frame charge toute l’application. -->

Contrats, communication, fallback

Le contrat d’un remote, c’est tout ce qui en dépend de l’extérieur : les clés de exposes, ce qu’elles exportent, les paramètres de route qu’elles lisent, les événements qu’elles émettent et les dépendances partagées. Traitez-le comme une API publique avec semver : un changement incompatible signifie une nouvelle clé (./routes-v2), et l’ancienne reste jusqu’à ce que le shell migre.

La communication, dans l’ordre où elle vaut la peine d’être essayée :

  1. L’URL — paramètres de route et query params. Gratuite, accessible par lien direct, elle survit à un rafraîchissement, et le remote n’importe rien d’un autre remote.
  2. Des événements typés sur window, depuis un minuscule paquet contracts (types + emit/listen). N’oubliez pas celui qui démarre tard : il doit demander l’état.
  3. Quelques petits Services, partagés en singletons — auth, utilisateur courant, feature flags — depuis une bibliothèque incluse dans shared.

À éviter : un Store global commun (le même Store NgRx, ou le même Service qui détient tout l’état, utilisé par chaque remote). Tout changement de la forme de l’état devient un changement coordonné entre équipes — exactement le couplage que vous vouliez supprimer. Chaque remote garde son propre état ; seuls des événements et des URL circulent entre eux (voir aussi State Management).

// 1. L’URL est le premier canal : gratuite, accessible par lien direct, survit à un rafraîchissement.
//    /catalog/audio?sort=price → le remote lit l’URL, pas un Store partagé.
//    (withComponentInputBinding() va dans le SHELL : les routes du remote
//    s’exécutent dans le routeur du shell)
export class ProductList {
  readonly category = input<string>();                 // depuis :category
  readonly sort = input<'price' | 'rating'>('rating'); // depuis ?sort=
}

// 2. La navigation entre remotes passe aussi par l’URL :
//    le remote n’importe rien d’un autre remote.
this.router.navigate(['/cart'], { queryParams: { added: product.id } });

Fallback. N’importe quel remote peut manquer : un déploiement raté, un CDN en panne, une erreur dans le manifest d’un environnement. Deux situations, deux solutions :

  • Une route entière : laissez loadChildren échouer et transformez l’erreur en redirection avec withNavigationErrorHandler. Un loader rejeté n’est pas mis en cache par le routeur, donc la navigation suivante réessaie le remote.
  • Un remote dans une page (un widget, un slot) : un composant du shell charge le remote avec resource, avec un timeout, un fallback et reload() pour « Réessayer » — comme dans le premier exemple.
// Une ROUTE de remote EN ÉCHEC : laissez loadChildren échouer et redirigez.
// Un loader rejeté n’est PAS mis en cache par le routeur → la navigation suivante réessaie.
import { ApplicationConfig, inject } from '@angular/core';
import {
  NavigationError,
  RedirectCommand,
  Router,
  provideRouter,
  withNavigationErrorHandler,
} from '@angular/router';
import type { NativeFederationResult } from '@angular-architects/native-federation';
import { routes } from './app.routes';

export const appConfig = (nf: NativeFederationResult): ApplicationConfig => ({
  providers: [
    provideRouter(
      routes(nf),
      withNavigationErrorHandler((e: NavigationError) => {
        const router = inject(Router);
        const target = `/indisponibil?url=${encodeURIComponent(e.url)}`;
        return new RedirectCommand(router.parseUrl(target));
      }),
    ),
  ],
});

// ✗ Le piège : un fallback renvoyé par le loader est un SUCCÈS pour le routeur.
//   Il est mis en cache sur la route jusqu’au rechargement de la page, donc « Réessayer »
//   n’atteint plus jamais le remote :
//   loadComponent: () => nf.loadRemoteModule(...).then(...).catch(() => RemoteDown)

Quand OUI, quand NON

La décision dépend de l’organisation plus que du code. Le score ci-dessous applique les règles de cette page : ce n’est pas une formule magique, mais il vous force à répondre aux questions qui comptent et vous montre pourquoi une recommandation ressort.

LiveFaut-il des micro frontends ?
Maturité de l’organisation
Situations particulières
Vous n’avez probablement pas besoin de micro frontends : Monorepo + lazy routes.
Monorepo + lazy routes (une seule application)85
  • + 3 équipes : des frontières dans le code (bibliothèques, règles d’import) suffisent généralement
  • + une release commune est acceptable
Native Federation (remotes Angular, runtime partagé)0
  • + à partir de ~4 équipes autonomes, l’autonomie commence à compter
  • − sans vrai besoin de déploiements indépendants, les MFE ne justifient pas leur prix
  • + même version d’Angular → @angular/core peut être un singleton partagé
  • − sans plateforme, sans pipelines par équipe ni monitoring : la complexité des MFE arrive en production
Web Components (Angular Elements, un runtime par bundle)0
iframes (isolation totale)0
Partez de votre situation réelle. Remarquez le poids du déploiement indépendant et de la maturité organisationnelle, comment « plusieurs versions majeures d’Angular » oriente la recommandation vers les Web Components, et comment le code non fiable impose un iframe.
Micro frontends
  • Plusieurs équipes autonomes (environ 4–5 ou plus) sur le même produit, avec des cadences de release différentes, qui se bloquent aujourd’hui.
  • Le déploiement indépendant est obligatoire (contractuel, réglementaire, rythmes différents) → Native Federation avec la même version d’Angular.
  • Migration progressive depuis une ancienne application → un nouveau shell + strangler fig ; les anciens écrans en Web Components ou iframes jusqu’à leur disparition.
  • Des produits séparés sous la même marque, avec une responsabilité claire par domaine métier.
  • Vous avez déjà les fondations : une équipe plateforme pour le shell et le design system, une CI/CD par équipe, un monitoring par version de remote.
Autre chose
  • Une équipe, ou quelques équipes qui peuvent livrer ensemble → une seule application dans un monorepo, avec des lazy routes et des frontières.
  • Le problème est la performance → @defer, lazy loading, SSR ; les MFE aggravent les choses.
  • Le problème est un code enchevêtré → bibliothèques, règles d’import, architecture ; les MFE ne font que déplacer l’enchevêtrement entre dépôts.
  • Vous voulez simplement réutiliser des composants entre applications → une bibliothèque publiée, ou partagée dans le monorepo.
  • Un widget pour des pages tierces → un seul Web Component (Angular Elements), pas une architecture.
  • Personne ne peut prendre en charge le shell, les contrats et les montées de version communes → reportez jusqu’à ce que ce soit le cas.

Pièges fréquents

  • La bibliothèque « partagée » qui n’est pas dans shared → chaque remote l’embarque séparément, donc providedIn: 'root' produit deux instances (deux classes, deux tokens). Incluez les bibliothèques avec état dans shared et vérifiez remoteEntry.json.
  • singleton: false sur @angular/core ou un remote sur une autre majeure → deux runtimes Angular. La même version partout ; pour les exceptions, l’isolation (Web Component, iframe).
  • strictVersion: false pour que ça « passe » → le message disparaît de la console, le problème reste et apparaît en production. Corrigez les versions, pas le flag.
  • Un fallback renvoyé par loadComponent (.catch(() => RemoteDown)) → le routeur le met en cache sur la route jusqu’au rafraîchissement, et « Réessayer » n’atteint jamais le remote. Laissez le loader échouer, ou utilisez un composant host avec reload().
  • Pas de timeout → un remoteEntry.json qui ne répond pas laisse la page en « chargement » pour toujours. Mettez un timeout sur chaque chargement de remote.
  • Un seul manifest pour tous les environnements → le staging charge les remotes de production (ou l’inverse). Un manifest par environnement, généré au déploiement.
  • Un Store global commun entre remotes → des releases en lockstep, un monolithe distribué. État local + événements + URL.
  • Un remote qui importe du code directement d’un autre remote → une dépendance cachée entre déploiements. Tout passe par libs/contracts ou par l’URL.
  • Des routes absolues écrites dans un remote (/products/42) → elles cassent quand le shell le monte sous un autre préfixe. Naviguez relativement à la route active.
  • Du CSS global dans les remotes → les styles d’une équipe cassent ceux d’une autre. Pas de styles globaux dans les remotes ; des tokens CSS issus du design system ; ShadowDom pour les Web Components.
  • Des erreurs sans contexte → vous ne savez pas quel remote, dans quelle version, a planté. Attachez le nom et la version du remote à chaque erreur remontée (voir gestion des erreurs).

Aide-mémoire

// REMOTE — federation.config.mjs
withNativeFederation({
  name: 'catalog',
  exposes: { './routes': './src/app/catalog.routes.ts' }, // API publique, versionnée
  shared: fromPackageJson({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
});

// SHELL — public/federation.manifest.json (un par environnement : dev / staging / prod)
// { "catalog": "https://catalog.exemplu.ro/remoteEntry.json" }

// SHELL — main.ts
initFederation('federation.manifest.json')
  .then((nf) => import('./bootstrap').then((m) => m.bootstrap(nf)));

// SHELL — routes
const catalog = {
  path: 'catalog',
  loadChildren: () =>
    nf.loadRemoteModule<{ CATALOG_ROUTES: Routes }>('catalog', './routes')
      .then((m) => m.CATALOG_ROUTES),
};

// VERSIONS : la même version d’Angular partout ; le host a priorité.
//   incompatible + strictVersion    → copie propre dans les scopes (NF)
//                                     "Unsatisfied version …" (webpack MF)
//   incompatible, sans strictVersion → utilise la version partagée + avertissement
// COMMUNICATION : URL → événements typés (libs/contracts) → petits Services partagés.
//   Pas de Stores communs.
// ÉCHEC : route → withNavigationErrorHandler + redirection
//          slot → composant host avec timeout + resource.reload()
// AUTRE MAJEURE / AUTRE FRAMEWORK → Web Component avec son propre runtime, ou iframe

Chap. 13 · 0 sur 4 terminées

Leçon terminée ?

Marquez-la : elle apparaîtra sur votre carte, dans le sommaire et sur la page d’accueil.

Se marque toute seule une fois la leçon lue en entier.

  1. Micro frontends
  2. Web Components avec Angular Elements
  3. Monorepo, bibliothèques et frontières
  4. Architecture hexagonale (ports et adapters)
Voir toute votre carte

Les 75 pages · tapez pour filtrer

  • 01Nouveautés d’Angular 21 & 22Tous les changements importants de la v21 et de la v22, en bref, avec un lien vers la page qui explique chacun.Départ
  • 01Votre première application, pas à pasUne liste de lecture construite d’un composant vide jusqu’à une application terminée — signals, @for, inputs et outputs, un service et un formulaire, pas à pas.Départ
  • 01Décisions rapides« Où mettre ça ? », « Signal ou Observable ? », « Shared ou feature ? » — des réponses courtes aux questions de tous les jours.Départ
  • 01Questions d’entretien Angular60 vraies questions d’entretien pour profils junior, confirmé et senior, avec la réponse à donner, la relance et les signaux d’alerte. Plus un entretien blanc.Départ
  • 02Standalone et bootstrapComment une application démarre sans NgModule : bootstrapApplication, app.config.ts, imports dans le composant.Fondamentaux modernes
  • 02Data binding et événementsInterpolation, bindings de propriétés et d’attributs, événements avec $event et filtres de touches, et comment [( )] est construit — ce qui entre dans le DOM et ce qui en revient.Fondamentaux modernes
  • 02Control Flow : @if, @for, @switchLa syntaxe de blocs qui remplace *ngIf/*ngFor/ngSwitch, avec track, @empty et vérifications d’exhaustivité.Fondamentaux modernes
  • 02@let et les nouvelles expressions de templateVariables locales avec @let, fonctions fléchées, spread, regex, instanceof, commentaires dans les balises.Fondamentaux modernes
  • 02@defer — Lazy Loading dans les templatesChargez les composants lourds uniquement quand c’est nécessaire : triggers, prefetch, placeholder, loading, error.Fondamentaux modernes
  • 02Bindings de classe et de stylePourquoi [class.x] et [style.x] ont remplacé ngClass et ngStyle (avec migration automatique depuis la v21).Fondamentaux modernes
  • 02Styles des composants et encapsulation:host, les trois modes d’encapsulation, les thèmes avec des variables CSS, et pourquoi ::ng-deep est un dernier recours.Fondamentaux modernes
  • 02Pipes : quand et commentPipes pures vs impures, pipes personnalisées, et comment choisir entre une pipe, computed() et une méthode.Fondamentaux modernes
  • 03signal() et computed()Les primitives réactives : état modifiable, état dérivé, égalité, untracked et règles d’immutabilité.Signals et réactivité
  • 03effect() — quand OUI, quand NONEffets de bord synchronisés avec les Signals : localStorage, logs, API impératives. Et leurs pièges.Signals et réactivité
  • 03linkedSignal() — état dérivé modifiableUn signal qui se réinitialise quand sa source change, mais que vous pouvez aussi modifier à la main.Signals et réactivité
  • 03resource, rxResource, httpResourceDes données asynchrones sous forme de Signals : status, value, error, reload, annulation automatique. Stable depuis la v22.Signals et réactivité
  • 03debounced() — signals différésLa version « debounce » d’un signal, sans RxJS. Parfait pour la recherche.Signals et réactivité
  • 03Listes : rechercher, filtrer, trier, paginerUne chaîne de signals computed qui ne recalcule que ce qui a changé, avec les filtres dans l’URL pour qu’un lien rouvre la même vue.Signals et réactivité
  • 03Les Observables depuis zéroCe qu’est un Observable, subscribe et unsubscribe, les opérateurs que vous utilisez chaque semaine, froid ou chaud — et quand un signal suffit.Signals et réactivité
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — et quand RxJS reste le bon choix.Signals et réactivité
  • 04input() et output()Les inputs comme Signals (required, transform, alias) et les outputs typés. Les données descendent, les événements remontent.Composants : communication
  • 04model() — binding bidirectionnelLe « banana-in-a-box » [( )] avec une seule API. Quand l’utiliser, et quand input + output est préférable.Composants : communication
  • 04viewChild et contentChildLes queries comme Signals : viewChild, viewChildren, contentChild, contentChildren, l’option read.Composants : communication
  • 04Lifecycle et DestroyRefQuels hooks comptent encore à l’ère des Signals, afterNextRender, DestroyRef et ngOnChanges typé.Composants : communication
  • 04Projection de contenung-content avec select, plusieurs slots, contenu par défaut et le piège de la projection conditionnelle.Composants : communication
  • 04ng-template et ngTemplateOutletLes templates comme valeurs : les render props en Angular, contexte typé et personnalisation des composants.Composants : communication
  • 05Smart vs DumbContainer vs présentation : qui détient l’état, qui se contente d’afficher, et comment bien les séparer.Conception des composants
  • 05Composants génériques <T>Un DataTable<T> ou un Select<T> correctement typé, avec des templates pour les cellules.Conception des composants
  • 05Composants dynamiquesNgComponentOutlet, createComponent avec inputBinding/outputBinding, et un tableau de bord construit à partir d’une configuration.Conception des composants
  • 05Directives et composition d’hôteLes directives d’attribut, les métadonnées host: {} et la Directive Composition API (hostDirectives).Conception des composants
  • 05Contrôles de formulaire personnalisésFormValueControl (Signal Forms) vs ControlValueAccessor : une notation par étoiles, dans les deux variantes.Conception des composants
  • 05Change DetectionOnPush est la valeur par défaut depuis la v22, Zone.js a disparu depuis la v21. Ce qui déclenche le rendu et comment éviter les mauvaises surprises.Conception des composants
  • 06inject() et @Service()Le nouveau décorateur @Service() de la v22, inject() vs le constructeur, et le contexte d’injection.Dependency Injection
  • 06Providers et hiérarchie des injecteursRoot vs route vs composant, useClass/useValue/useFactory/useExisting, et les modificateurs optional/self/skipSelf.Dependency Injection
  • 06InjectionToken et provideX()Configuration typée, fonctions provideFeature(), makeEnvironmentProviders et initializers.Dependency Injection
  • 06injectAsync() — services en Lazy LoadingChargez un service lourd uniquement quand vous en avez besoin, avec prefetch à l’idle.Dependency Injection
  • 07Structure des dossierscore / shared / features, ce que contient chacun, et à quoi ressemble l’intérieur d’une feature.Architecture d’application
  • 07Shared vs featureQuand déplacer un composant dans shared et (surtout) quand NE PAS le faire. Une checklist concrète.Architecture d’application
  • 07Interfaces et modèlesInterface en ligne ou fichier séparé ? DTO vs modèle de domaine, type vs interface, où les ranger.Architecture d’application
  • 07State Management avec les SignalsStores simples à base de Signals, facades, état local vs global, et quand NgRx devient nécessaire.Architecture d’application
  • 07Guide de style et nommageLes conventions officielles actuelles : noms de fichiers sans suffixes, protected, readonly, ordre des membres.Architecture d’application
  • 07Barrels, alias et frontièresindex.ts, alias de chemins, API publique d’une feature et règles de dépendances vérifiables automatiquement.Architecture d’application
  • 07Erreurs : @boundary et ErrorHandlerLe nouveau bloc @boundary de la v22.2, l’ErrorHandler global, les erreurs HTTP et les erreurs de resource.Architecture d’application
  • 08Routing moderne et Lazy LoadingloadComponent, routes enfants, paramètres comme inputs et héritage des paramètres (le nouveau comportement par défaut de la v22).Routing
  • 08Guards et resolverscanActivate, canMatch, canDeactivate et resolve sous forme de fonctions, avec redirections via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — les basesDes formulaires bâtis sur les Signals : form(), [formField], validation par schéma. Stable depuis la v22.Formulaires
  • 09Signal Forms — avancéValidation personnalisée et inter-champs, async/HTTP avec debounce, listes dynamiques, désactivé/masqué, schémas réutilisables.Formulaires
  • 09Reactive Forms typésFormGroup typé, nonNullable, FormArray — et leur cohabitation avec les Signal Forms.Formulaires
  • 10HttpClient, interceptors et couche d’APIHttpClient sans configuration (v21), Fetch par défaut (v22), interceptors fonctionnels et une couche d’API avec mapping des DTO.HTTP et données
  • 11Performance — une checklistCe qui compte vraiment : OnPush + Signals, track, @defer, routes en Lazy Loading, images, bundles et Hydration.Qualité et performance
  • 11Tests avec VitestVitest est le runner par défaut depuis la v21. TestBed, composants avec Signals, services, HTTP et harnesses.Qualité et performance
  • 11DevTools : regarder dans votre applicationChrome DevTools et Angular DevTools pas à pas : ce que montre chaque panneau, avec des exercices qui fonctionnent sur ce site.Qualité et performance
  • 11Angular AriaDes composants headless et accessibles que vous stylez comme vous voulez : tabs, accordion, menu, listbox...Qualité et performance
  • 12SSR, Prerendering et modes de renduQuand rendre sur le serveur, quand au build et quand uniquement dans le navigateur : RenderMode par route, app.routes.server.ts et les coûts réels.SSR et Hydration
  • 12Hydration : event replay et incrémentaleComment le navigateur reprend le HTML du serveur sans le re-rendre : Hydration complète, event replay, @defer (hydrate on …) et erreurs de mismatch.SSR et Hydration
  • 12Code compatible serveurwindow n’existe pas sur le serveur : PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, le cache HTTP et PendingTasks.SSR et Hydration
  • 13Micro frontendsQuand découper une application en plusieurs déploiements vaut le coup, et quand c’est juste de la complexité : Native Federation, Module Federation, iframes, Web Components.Architecture à grande échelle
  • 13Web Components avec Angular ElementscreateCustomElement : un composant Angular utilisé comme balise HTML n’importe où, plus la communication entre applications via des événements et BroadcastChannel.Architecture à grande échelle
  • 13Monorepo, bibliothèques et frontièresUn workspace avec plusieurs applications et bibliothèques : ng generate library, Nx, points d’entrée secondaires, publication et règles de dépendances.Architecture à grande échelle
  • 13Architecture hexagonale (ports et adapters)Un domaine sans Angular, des ports sous forme de classes abstraites et des adapters interchangeables : HTTP, localStorage ou en mémoire, sans toucher à l’UI.Architecture à grande échelle
  • 14Les Signals sous le capotLe graphe réactif : producteurs et consommateurs, push-pull, pourquoi computed est lazy et sans glitch, le problème du diamant et quand les effects s’exécutent.Réactivité avancée
  • 14RxJS avancé : opérateurs d’ordre supérieurswitchMap, mergeMap, concatMap et exhaustMap sur un diagramme interactif, plus retry avec backoff, shareReplay et comment éviter les fuites.Réactivité avancée
  • 14NgRx SignalStoreState Management à grande échelle sur les Signals : withState, withComputed, withMethods, rxMethod, entités et hooks — et quand un store simple suffit.Réactivité avancée
  • 14Machines à états avec les signalsFini les états impossibles : une machine typée sur des signals, avec transitions, gardes et un diagramme d’états en direct.Réactivité avancée
  • 15WebSockets : un temps réel qui résiste au réseauUn socket typé sous forme de signals : reconnexion avec backoff et jitter, heartbeat, file d’attente hors ligne et sécurité SSR.Temps réel et hors ligne
  • 15Mises à jour optimistes et rollbackUne UI instantanée qui reste honnête : snapshot et rollback, réponses dans le désordre, clés d’idempotence.Temps réel et hors ligne
  • 15Offline-first : Service Worker et cacheLe Service Worker d’Angular : stratégies de cache, annonce des nouvelles versions et file d’attente pour les modifications faites hors ligne.Temps réel et hors ligne
  • 16Web WorkersDes calculs lourds sans bloquer l’UI : un vrai worker dans la page, l’échange de messages et quand ça n’en vaut pas la peine.UI avancée et performance
  • 16Listes géantes : virtual scroll100 000 lignes sans latence : le virtual scroll du CDK, itemSize, trackBy et les alternatives (pagination, scroll infini).UI avancée et performance
  • 16Animations modernes et View Transitionsanimate.enter et animate.leave à la place de @angular/animations (déprécié), transitions CSS et View Transitions à la navigation.UI avancée et performance
  • 16Fuites mémoire : les trouver et les corrigerAbonnements, timers et listeners qui survivent à leur composant — comment les repérer dans les DevTools et les fermer.UI avancée et performance
  • 17Sécurité : XSS, sanitisation et CSPCe qu’Angular fait automatiquement contre le XSS, quand bypassSecurityTrust devient dangereux, plus la CSP avec nonce, Trusted Types et XSRF.Sécurité et internationalisation
  • 17Authentification : tokens et refreshUn interceptor qui ajoute le token, le renouvelle une seule fois sur une 401 et rejoue les requêtes en attente — plus où conserver le token.Sécurité et internationalisation
  • 17OAuth 2.1 et OpenID Connect avec PKCELe flux authorization code avec PKCE, pas à pas : verifier et challenge, state et nonce, l’ID token, et pourquoi un BFF est plus sûr.Sécurité et internationalisation
  • 17Internationalisation (i18n)$localize, l’attribut i18n, ICU plural/select, un build par langue vs des traductions à l’exécution, et le formatage selon la locale.Sécurité et internationalisation