Créer une architecture Micro Frontend avec Angular

Créer une architecture Micro Frontend avec Angular

Créer une architecture Micro Frontend avec Angular, ce n’est pas seulement une affaire de technologie ou de découpage en plusieurs applications. C’est surtout une manière de repenser la façon dont une équipe construit, fait évoluer et déploie une interface utilisateur. Quand un projet grandit, quand les écrans se multiplient, quand les délais se raccourcissent et que plusieurs équipes doivent travailler en parallèle sans se marcher dessus, l’architecture classique d’un front-end monolithique montre vite ses limites. On commence par rajouter des modules, puis des couches, puis des conventions, puis des exceptions. Au début, cela semble raisonnable. Mais un jour, une petite modification dans une partie du code casse une autre partie, un déploiement devient risqué, la base de code ralentit, et la collaboration entre équipes devient plus lourde que productive. C’est souvent à ce moment-là qu’une architecture Micro Frontend prend tout son sens.

Avec Angular, cette approche est particulièrement intéressante, parce que le framework apporte déjà une structure forte, un système de modules, un routage mature, un écosystème solide pour le lazy loading, ainsi qu’une intégration naturelle avec les outils modernes de monorepo et de fédération de modules. Pourtant, adopter les Micro Frontends ne veut pas dire fractionner une application au hasard. Il faut d’abord comprendre le besoin métier, identifier les frontières fonctionnelles, définir les responsabilités de chaque équipe et choisir le bon niveau de découpage. Mal pensé, un Micro Frontend peut devenir une collection chaotique d’applications difficiles à maintenir. Bien pensé, il devient une architecture élégante, robuste et étonnamment agréable à faire évoluer.

Ce guide propose une vision complète, pratique et concrète de la création d’une architecture Micro Frontend avec Angular. Nous allons voir pourquoi cette approche existe, quand l’adopter, comment structurer les projets, comment partager des composants et des dépendances, comment gérer la communication entre applications, comment orchestrer le tout au niveau du shell principal, et comment éviter les pièges les plus fréquents. Le but n’est pas seulement de donner une recette technique, mais de montrer comment construire une base saine qui respecte à la fois les besoins des développeurs, des équipes produit et des utilisateurs finaux.

Comprendre la logique des Micro Frontends

Un Micro Frontend est l’extension du principe des microservices au front-end. L’idée est simple à énoncer, mais riche dans ses implications : au lieu d’avoir une seule grande application front-end qui contient toute l’expérience utilisateur, on découpe l’interface en plusieurs parties autonomes, chacune responsable d’un domaine métier ou d’un périmètre fonctionnel précis. Chaque partie peut être développée, testée et déployée indépendamment, à condition de respecter certaines règles d’intégration.

Ce modèle apporte plusieurs avantages immédiats. D’abord, il permet aux équipes de travailler en parallèle avec moins de dépendances croisées. Ensuite, il aide à réduire la taille des bases de code individuelles, ce qui améliore la lisibilité et la maintenabilité. Enfin, il ouvre la porte à des déploiements plus fréquents et plus ciblés. Lorsqu’une équipe corrige un problème dans son périmètre, elle n’a pas forcément besoin de reconstruire et redéployer tout le front-end de l’entreprise.

Cependant, il faut être honnête : les Micro Frontends ne sont pas une solution magique. Ils ajoutent aussi de la complexité. Il faut penser à l’intégration à runtime, à l’uniformité visuelle, au partage des dépendances, à l’authentification, au routage, à la performance, à la gestion des erreurs, et parfois même à la cohérence des versions de framework. Autrement dit, on échange un monolithe simple à comprendre contre une constellation d’applications plus flexibles, mais plus exigeantes à orchestrer.

Le vrai gain apparaît lorsque le produit est suffisamment grand pour que le découpage en domaines ait du sens. Une petite application de quelques pages n’a généralement pas besoin de Micro Frontends. Mais une plateforme e-commerce, une suite SaaS complexe, un portail bancaire, une console d’administration ou une marketplace interne avec plusieurs équipes peut en bénéficier énormément.

Pourquoi Angular se prête bien à cette architecture

Angular est souvent un excellent choix pour les Micro Frontends, pour plusieurs raisons. Le framework a été conçu autour d’une architecture modulaire, avec une forte discipline dans l’organisation du code. Les concepts de modules, de lazy loading, de services injectables, de routing et de composants réutilisables facilitent le découpage fonctionnel. Là où d’autres écosystèmes nécessitent davantage de conventions externes, Angular apporte une structure naturelle qui aide à garder le projet lisible.

Un autre point important est la maturité de l’écosystème. Angular s’intègre très bien avec des outils comme Nx, qui permet de gérer des monorepos de grande taille, ou avec Webpack Module Federation, qui offre une base technique solide pour charger des parties d’application à la volée. Angular a aussi bénéficié d’évolutions qui rendent les architectures modernes plus réalistes, notamment l’amélioration du lazy loading, l’arrivée des standalone components, et une meilleure souplesse pour organiser les frontières entre zones fonctionnelles.

L’autre raison, plus humaine celle-là, est que les équipes Angular sont souvent habituées à travailler de manière structurée. Cela ne garantit pas le succès, bien sûr, mais cela aide beaucoup. Un Micro Frontend réussi n’est pas seulement une prouesse technique, c’est une discipline collective. Angular donne un cadre qui permet de mieux la pratiquer.

Quand choisir les Micro Frontends

Il est tentant d’adopter une architecture Micro Frontend parce qu’elle semble moderne, scalable et séduisante. Pourtant, le bon moment compte énormément. Cette approche devient pertinente lorsque plusieurs conditions sont réunies. La première, c’est la taille du produit. Si une seule équipe gère tout le front-end et que l’application reste modeste, l’intérêt est limité. La seconde, c’est la présence de frontières métier claires. Si vous pouvez identifier des domaines bien séparés — par exemple catalogue, panier, paiement, profil utilisateur, back-office — le découpage devient naturel. La troisième, c’est la nécessité d’autonomie entre équipes. Quand plusieurs squads doivent publier indépendamment, les Micro Frontends prennent beaucoup de valeur.

Il existe aussi des signaux d’alerte qui montrent qu’une architecture plus modulaire serait utile. Si les merge conflicts sont fréquents, si les releases sont bloquées par des dépendances entre équipes, si l’ensemble du front-end devient trop lourd à faire évoluer, ou si chaque changement nécessite une coordination excessive, alors le sujet mérite une réflexion sérieuse. En revanche, si le produit est encore simple, il peut être plus raisonnable de rester sur une architecture modulaire classique et de préparer proprement le terrain pour une éventuelle évolution future.

La décision doit être guidée par la réalité de l’organisation, pas seulement par l’attrait d’une technologie. Les Micro Frontends résolvent surtout un problème de scalabilité organisationnelle. Ils sont donc particulièrement efficaces lorsque les équipes ont besoin de liberté, mais que cette liberté doit être encadrée par un contrat technique clair.

Les modèles d’architecture possibles

Il existe plusieurs manières de structurer une architecture Micro Frontend avec Angular. Le choix dépend de votre contexte, de votre niveau d’exigence en matière d’indépendance, et de la maturité de vos équipes.

Le premier modèle est celui du shell central. Une application principale, souvent appelée container ou shell, orchestre l’affichage, le routing global, l’authentification et parfois le layout. Les Micro Frontends viennent se greffer dessus sous forme de modules distants chargés dynamiquement. C’est souvent le modèle le plus équilibré, car il combine autonomie locale et cohérence globale.

Le deuxième modèle repose sur un assemblage côté serveur ou via une couche de composition plus externe. Cela peut être intéressant dans certains contextes, mais avec Angular et une logique front-end orientée SPA, le shell central reste généralement plus pratique.

Le troisième modèle est celui du découpage très poussé, où chaque domaine possède presque son propre univers front-end complet. Cette approche fonctionne pour des organisations très grandes, mais elle réclame une gouvernance forte, des conventions claires et une vraie maturité technique. Sinon, l’expérience utilisateur peut devenir incohérente et le coût de maintenance exploser.

Dans la plupart des cas, le meilleur compromis consiste à garder un shell Angular principal et à brancher des Micro Frontends par domaine métier. Le shell gère le cadre, les parties métiers gèrent leur logique, et les règles communes sont partagées avec parcimonie.

Définir les frontières fonctionnelles

Le succès d’une architecture Micro Frontend dépend surtout de la qualité du découpage. Il faut penser en domaines métier et non en couches techniques. Une erreur fréquente consiste à découper par type de composant, par exemple une équipe pour les boutons, une autre pour les formulaires, une autre pour les listes. Ce type de découpage crée souvent des dépendances artificielles et empêche l’autonomie. Mieux vaut découper par responsabilité métier : espace client, commande, support, facturation, administration, etc.

Chaque Micro Frontend doit idéalement avoir un périmètre fonctionnel compréhensible par une équipe. Cette équipe doit être capable de faire évoluer sa partie sans devoir négocier en permanence avec les autres pour des détails internes. Le but n’est pas l’isolement total, mais la responsabilité claire. Plus les frontières sont nettes, plus l’architecture devient saine.

Un bon découpage prend aussi en compte les parcours utilisateur. Il ne suffit pas de regarder l’organigramme des équipes ; il faut observer comment les utilisateurs naviguent. Parfois, un domaine métier correspond naturellement à plusieurs pages dans le produit. Parfois, une seule page mélange plusieurs fonctions. Le travail d’architecture consiste alors à trouver le bon niveau d’agrégation.

Une structure de projet réaliste

Imaginons une application Angular composée d’un shell principal et de trois Micro Frontends : catalog, checkout et account. Le shell gère la navigation, la barre supérieure, la session utilisateur et les routes principales. Chaque Micro Frontend possède son propre code métier, ses composants, ses services, ses routes et éventuellement ses tests.

Une structure de monorepo pourrait ressembler à cela :

apps/
  shell/
  catalog/
  checkout/
  account/
libs/
  ui/
  auth/
  shared-utils/
  data-access/

Le dossier apps contient les applications déployables, tandis que libs contient les briques partagées. Cette approche est très naturelle avec Nx, mais elle peut aussi être reproduite dans une organisation plus classique. L’important n’est pas l’outil, mais la discipline. Les bibliothèques partagées doivent rester légères, stables et orientées vers des abstractions réellement utiles. Il ne faut pas transformer shared en fourre-tout, sinon on recrée un monolithe caché.

Le shell doit rester responsable du cadre global. Les Micro Frontends, eux, doivent concentrer la logique de domaine. Les bibliothèques partagées doivent être réservées aux éléments transverses : composants UI communs, contrats de données, utilitaires, services de session, helpers de formatage, ou éventuellement design system.

Module Federation avec Angular

Pour charger des Micro Frontends à runtime, l’approche la plus utilisée dans l’écosystème Angular moderne est Webpack Module Federation. L’idée consiste à exposer certaines parties d’une application distante, puis à les consommer dans le shell principal sans compiler tout ensemble à chaque fois.

Dans un environnement Angular, cette logique est souvent mise en place avec des helpers dédiés qui simplifient la configuration du webpack.config.js ou de son équivalent. Le principe reste le même : une application expose des modules, et une autre les importe dynamiquement.

Voici un exemple simplifié de configuration côté application distante :

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  output: {
    uniqueName: "catalog",
    publicPath: "auto",
  },
  optimization: {
    runtimeChunk: false,
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "catalog",
      filename: "remoteEntry.js",
      exposes: {
        "./CatalogModule": "./src/app/catalog/catalog.module.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" },
      },
    }),
  ],
};

Côté shell, on déclare les remotes :

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      remotes: {
        catalog: "catalog@http://localhost:4201/remoteEntry.js",
        checkout: "checkout@http://localhost:4202/remoteEntry.js",
        account: "account@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" },
      },
    }),
  ],
};

Dans la pratique, vous devrez adapter la configuration à votre version d’Angular et à votre outillage, mais la logique générale reste la même : chaque application distante publie un point d’entrée, et le shell le charge à la demande.

L’intérêt de cette approche est majeur. Elle permet aux équipes de livrer indépendamment. Le catalogue peut évoluer sans obliger l’équipe checkout à reconstruire sa partie. Le shell orchestre, mais il ne contient pas toute la logique métier.

Charger un micro frontend dans le routing Angular

Une manière élégante d’intégrer un Micro Frontend consiste à l’attacher au routing. Le shell garde la main sur les grandes routes, et chaque route peut charger dynamiquement une application distante.

Un exemple de configuration de routes dans le shell pourrait ressembler à ceci :

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'catalog',
    loadChildren: () =>
      import('catalog/CatalogModule').then(m => m.CatalogModule),
  },
  {
    path: 'checkout',
    loadChildren: () =>
      import('checkout/CheckoutModule').then(m => m.CheckoutModule),
  },
  {
    path: 'account',
    loadChildren: () =>
      import('account/AccountModule').then(m => m.AccountModule),
  },
  {
    path: '',
    redirectTo: 'catalog',
    pathMatch: 'full',
  },
];

Ce schéma est très intuitif pour l’utilisateur et très confortable pour les équipes. Chaque domaine a sa route, sa logique, son cycle de déploiement. Le shell ne fait qu’assembler les morceaux au bon moment. En plus, le lazy loading évite de charger inutilement toute l’application dès le premier affichage. On garde ainsi une bonne performance de départ, ce qui est crucial pour l’expérience utilisateur.

Il faut cependant veiller à la cohérence des transitions. Quand une route distante prend du temps à se charger, il est essentiel d’afficher un état de chargement clair, ou un squelette d’interface. Un écran blanc ou une transition brutale donne immédiatement l’impression d’une application fragile.

Créer un module distant Angular

Du côté d’un Micro Frontend, il faut exposer un module ou un ensemble de routes. Voici un exemple simple :

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CatalogHomeComponent } from './catalog-home/catalog-home.component';
import { ProductListComponent } from './product-list/product-list.component';

const routes: Routes = [
  {
    path: '',
    component: CatalogHomeComponent,
  },
  {
    path: 'products',
    component: ProductListComponent,
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class CatalogModule {}

Puis on expose ce module via Module Federation. L’intérêt est que le module reste autonome. Il gère ses propres composants, ses propres services et sa logique de navigation interne. Le shell n’a pas besoin de connaître tous ses détails.

Cette autonomie est précieuse, mais elle ne doit pas devenir une excuse pour dupliquer sans limite. Il faut trouver un équilibre entre indépendance locale et cohérence globale. L’utilisateur final doit avoir l’impression d’utiliser un seul produit, pas quatre applications juxtaposées.

Partager des composants UI sans casser l’autonomie

Le partage de composants est une question sensible. Beaucoup d’équipes sont tentées de créer une bibliothèque UI commune très riche, avec des dizaines de composants réutilisables. Sur le papier, cela paraît rationnel. En réalité, trop de partage peut créer une dépendance forte entre équipes et freiner l’évolution. Le bon réflexe est de partager ce qui est stable, générique et réellement transversal.

Par exemple, partager un bouton, un champ de formulaire standard, une card de base, un système de typographie ou un design token peut être utile. En revanche, partager un composant très spécifique à un domaine métier est souvent une mauvaise idée. Mieux vaut laisser cette logique vivre dans son Micro Frontend.

Une bibliothèque commune pourrait ressembler à ceci :

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-primary-button',
  template: `
    <button class="btn btn-primary" [disabled]="disabled">
      <ng-content></ng-content>
    </button>
  `,
})
export class PrimaryButtonComponent {
  @Input() disabled = false;
}

Ce composant est volontairement simple. Il ne contient aucune logique métier. Il sert à assurer une cohérence visuelle. Si vous le faites évoluer, il faut le faire avec précaution, comme un contrat partagé.

Le plus souvent, les équipes gagnent à mettre en place un design system clair, mais pas trop intrusif. Le design system fournit le langage visuel, tandis que chaque Micro Frontend reste responsable de son comportement métier.

Gérer la communication entre Micro Frontends

La communication est l’un des sujets les plus délicats. Dès que plusieurs applications cohabitent dans un même shell, il faut définir comment elles échangent des informations sans créer une toile d’araignée de dépendances.

Une première approche consiste à faire transiter les données par le shell. Le shell observe les événements importants et transmet les états nécessaires aux applications distantes via des services partagés ou des inputs d’orchestration. C’est souvent la solution la plus claire pour les données globales comme l’utilisateur connecté, la langue active, le thème ou les permissions.

Une autre approche utilise des événements personnalisés sur window ou un bus d’événements léger. Cela peut être pratique, mais il ne faut pas en abuser. Une architecture trop événementielle devient vite difficile à suivre si aucun contrat n’est défini.

Voici un exemple simple de service d’événements :

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class EventBusService {
  private eventSubject = new Subject<{ type: string; payload?: any }>();
  events$ = this.eventSubject.asObservable();

  emit(type: string, payload?: any) {
    this.eventSubject.next({ type, payload });
  }
}

Puis un Micro Frontend peut écouter ou émettre des événements :

constructor(private eventBus: EventBusService) {}

ngOnInit() {
  this.eventBus.events$.subscribe(event => {
    if (event.type === 'USER_UPDATED') {
      console.log('Nouvel utilisateur', event.payload);
    }
  });
}

save() {
  this.eventBus.emit('CART_UPDATED', { count: 3 });
}

Cette méthode est simple, mais elle doit rester encadrée. L’événement est un moyen de communication, pas une excuse pour contourner toute architecture. Il vaut mieux définir quelques événements de domaine bien nommés que multiplier les messages flous.

Une autre option plus propre dans certains cas consiste à utiliser un contrat de données basé sur des services partagés, ou une couche d’état centralisée légère. Cela dépend du niveau d’interaction entre les Micro Frontends. Plus les échanges sont fréquents, plus il faut réfléchir à une stratégie structurée.

Authentification et autorisation

L’authentification est souvent gérée au niveau du shell, car elle concerne l’ensemble de l’expérience. Le shell peut vérifier le token, récupérer le profil utilisateur, et transmettre le contexte de session aux Micro Frontends. Chaque application distante peut ensuite utiliser ce contexte pour adapter ses écrans.

Un service d’authentification simple pourrait être partagé de cette manière :

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private tokenKey = 'access_token';

  getToken(): string | null {
    return localStorage.getItem(this.tokenKey);
  }

  isAuthenticated(): boolean {
    return !!this.getToken();
  }

  logout(): void {
    localStorage.removeItem(this.tokenKey);
    window.location.href = '/login';
  }
}

Dans un contexte réel, il faudra évidemment aller plus loin : gestion des refresh tokens, sécurisation des flux, éventuellement OAuth2 ou OpenID Connect, protection contre les expirations, synchronisation multi-onglets, et contrôle d’accès par rôle. Le point essentiel reste le suivant : le shell doit poser la base de confiance, mais chaque Micro Frontend doit vérifier ce qu’il a le droit d’afficher.

On gagne énormément en clarté quand les règles d’autorisation sont définies à la fois côté shell et côté domaine. Le shell peut cacher une route, mais le Micro Frontend ne doit jamais supposer naïvement que tout est autorisé. La sécurité ne doit pas dépendre uniquement de l’interface.

La gestion des erreurs et des indisponibilités

Dans une architecture Micro Frontend, un composant distant peut parfois être indisponible. Le réseau peut échouer, le service peut être déployé avec une erreur, ou une version incompatible peut être chargée. Il faut donc prévoir des mécanismes de repli.

Le shell peut afficher un message générique, une page de secours ou une version dégradée de l’expérience. Par exemple, si le module checkout ne se charge pas, l’utilisateur devrait voir une explication claire plutôt qu’un écran cassé. C’est un détail technique, mais c’est aussi un détail de confiance. Une interface qui sait tomber proprement inspire plus de sérénité qu’une interface qui s’effondre en silence.

Voici une façon simple de gérer un chargement de route avec fallback :

loadRemoteModule({
  type: 'module',
  remoteEntry: 'http://localhost:4201/remoteEntry.js',
  exposedModule: './CatalogModule'
})
  .then(m => m.CatalogModule)
  .catch(err => {
    console.error('Erreur de chargement du micro frontend', err);
    return import('./fallback/fallback.module').then(m => m.FallbackModule);
  });

Le fallback peut être une page d’erreur, un composant informatif, ou même un contenu statique minimal. L’important est de maîtriser l’expérience, même dans les cas dégradés.

Il est aussi conseillé de centraliser la journalisation des erreurs. Quand plusieurs Micro Frontends coexistent, le diagnostic devient beaucoup plus simple si les logs remontent dans un système commun d’observabilité.

Déploiement indépendant et versioning

L’un des grands atouts des Micro Frontends est le déploiement indépendant. Chaque équipe peut publier sa partie sans attendre un grand cycle de release global. Mais pour que cela fonctionne, il faut penser au versioning des contrats et à la compatibilité entre shell et remotes.

Si le shell attend une exposition nommée CatalogModule et qu’une nouvelle version du remote la renomme brutalement, tout casse. Les contrats d’intégration doivent donc être considérés avec le même sérieux que des API backend. Une bonne pratique consiste à versionner les points d’entrée importants, à documenter les remotes exposés, et à éviter les changements cassants sans période de transition.

Dans certains contextes, vous pouvez même déployer les Micro Frontends avec des stratégies de compatibilité progressive. Par exemple, une nouvelle version du shell peut consommer à la fois l’ancienne et la nouvelle version d’un remote pendant une période donnée. Cela réduit le risque de rupture.

Au niveau de l’infrastructure, chaque application peut être déployée sur son propre pipeline CI/CD. Le shell référence alors des URLs ou des manifestes de déploiement. C’est très puissant, mais cela implique une rigueur accrue sur la gestion des environnements, des caches et des assets statiques.

Gérer les dépendances partagées

Le partage des dépendances est un point central avec Module Federation. Angular, RxJS et certaines bibliothèques communes devraient généralement être partagées en singleton afin d’éviter les doublons et les comportements incohérents. En revanche, il faut rester attentif aux versions. Une mauvaise configuration peut provoquer des bugs étranges difficiles à diagnostiquer.

Dans beaucoup de projets, on configure les dépendances partagées comme suit :

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: true, requiredVersion: "auto" },
}

Le mode singleton évite d’avoir plusieurs instances d’une même bibliothèque dans le runtime. strictVersion permet de faire respecter les compatibilités. Cela paraît parfois rigide, mais c’est souvent ce qui évite les problèmes les plus coûteux.

Cela dit, il faut accepter qu’un peu de discipline soit nécessaire. Si chaque équipe décide de faire évoluer Angular à sa manière, l’architecture s’abîme vite. Les Micro Frontends donnent de l’autonomie, mais pas une liberté totale et anarchique. Il reste des décisions communes à prendre, notamment sur le noyau technique partagé.

Le rôle du monorepo

Le monorepo est souvent un excellent compagnon des Micro Frontends Angular. Il permet de centraliser les projets, les bibliothèques partagées, les règles de linting, les tests et parfois les pipelines. Avec un outil comme Nx, on peut gérer plusieurs applications et plusieurs libs dans un espace cohérent, tout en conservant des frontières claires.

Le monorepo simplifie certaines choses, comme le partage de code ou la standardisation des outils. Il rend aussi le refactoring plus sûr, car les dépendances sont visibles et les relations entre projets peuvent être analysées. En revanche, il ne faut pas confondre monorepo et monolithe. Le monorepo est une organisation de dépôt ; il ne dit pas comment votre architecture runtime fonctionne. Vous pouvez avoir un monorepo très structuré avec plusieurs applications réellement indépendantes.

C’est justement là que réside son intérêt : il offre une base confortable pour collaborer, tout en laissant la possibilité de déployer séparément. Pour beaucoup d’équipes, c’est le meilleur des deux mondes.

Tests : unitaires, d’intégration et contractuels

Une architecture Micro Frontend sérieuse doit être testée à plusieurs niveaux. Les tests unitaires valident les composants et les services internes de chaque domaine. Les tests d’intégration vérifient que les modules se chargent correctement et que les interactions avec le shell fonctionnent. Les tests contractuels, eux, sont essentiels pour sécuriser les points d’intégration entre micro applications.

Par exemple, un test unitaire sur un composant de catalogue peut ressembler à ceci :

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductListComponent } from './product-list.component';

describe('ProductListComponent', () => {
  let component: ProductListComponent;
  let fixture: ComponentFixture<ProductListComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ProductListComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(ProductListComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

Un test d’intégration, lui, peut vérifier que la route catalog charge bien le module distant. Pour cela, il faut souvent simuler ou configurer l’environnement de test de manière adaptée. Les tests contractuels deviennent particulièrement utiles lorsqu’une équipe dépend d’un module exposé par une autre. Ils permettent de valider que le nom du module, les routes, les événements ou les structures de données n’ont pas changé de manière cassante.

Dans une architecture distribuée, tester tôt et souvent n’est pas une option. C’est une condition de survie.

Performance et chargement progressif

L’un des risques les plus souvent cités avec les Micro Frontends concerne la performance. Après tout, si l’on charge plusieurs applications, plusieurs bundles et plusieurs dépendances, le coût peut vite grimper. Ce risque est réel, mais il peut être maîtrisé avec une bonne stratégie.

La première règle est de charger seulement ce qui est nécessaire. Le lazy loading doit être systématique. La deuxième est de surveiller la taille des bundles partagés. La troisième est d’éviter les dépendances redondantes ou trop lourdes. La quatrième est de s’assurer que le shell donne une impression de fluidité dès le premier affichage.

Vous pouvez aussi optimiser l’expérience avec des skeleton screens, des placeholders, du préchargement intelligent et des stratégies de cache. Par exemple, il peut être pertinent de précharger un Micro Frontend lorsque l’utilisateur approche d’une zone probable de navigation. L’idée est d’anticiper légèrement sans charger toute l’application inutilement.

Voici un exemple de préchargement simple :

import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

export class CustomPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    return route.data?.['preload'] ? load() : of(null);
  }
}

Et dans les routes :

{
  path: 'catalog',
  loadChildren: () => import('catalog/CatalogModule').then(m => m.CatalogModule),
  data: { preload: true }
}

Cela permet de trouver un équilibre entre performance perçue et rapidité de navigation.

Design system et cohérence visuelle

Plus votre architecture se découpe, plus la cohérence visuelle devient importante. Les utilisateurs ne doivent pas sentir qu’ils passent d’une application à une autre à chaque clic. Le design system joue ici un rôle crucial. Il fournit les règles communes de typographie, d’espacement, de couleur, de composants et d’interactions.

Dans un contexte Micro Frontend, il vaut mieux définir un socle commun léger mais solide. Ce socle peut être une bibliothèque partagée ou un ensemble de tokens de design consommés par chaque équipe. L’idée n’est pas d’interdire toute variation, mais de garantir une expérience homogène.

Une simple variable CSS partagée peut déjà faire beaucoup :

:root {
  --color-primary: #2f6fed;
  --color-surface: #ffffff;
  --color-text: #1f2937;
  --spacing-unit: 8px;
}

Chaque Micro Frontend peut ensuite consommer ces variables sans dupliquer les choix graphiques de base. La cohérence passe aussi par les comportements : états de chargement, messages d’erreur, validation des formulaires, navigation, accessibilité. Une architecture bien pensée ne se limite pas au code, elle se prolonge dans l’expérience.

Accessibilité et expérience utilisateur

Dans une architecture éclatée, l’accessibilité est parfois négligée, car chacun suppose que l’autre a pensé au problème. C’est une erreur. Chaque Micro Frontend doit respecter les standards d’accessibilité de base : navigation clavier, contraste suffisant, labels explicites, structure sémantique, annonces pour les changements dynamiques.

Le shell peut aussi aider en fournissant des composants globaux accessibles et des conventions communes. Mais la responsabilité finale reste locale. Si un Micro Frontend introduit un formulaire inaccessible ou un flux impossible à utiliser sans souris, tout le produit en souffre.

Le point humain est essentiel ici : une architecture moderne n’a de valeur que si elle améliore réellement la vie des utilisateurs et des équipes. Une grande sophistication technique ne compense jamais une mauvaise expérience.

Un exemple d’orchestration complète

Prenons un scénario simple. Un utilisateur arrive sur le shell Angular. Le shell vérifie l’authentification, charge la barre de navigation commune, puis expose trois routes principales. Lorsqu’il clique sur “Catalogue”, le Micro Frontend catalog se charge dynamiquement. Celui-ci affiche ses propres pages, récupère ses données via ses services, et émet éventuellement un événement lorsque l’utilisateur ajoute un produit au panier. Le shell ou un autre Micro Frontend peut écouter cet événement pour mettre à jour l’icône panier.

Voici une version simplifiée d’un composant catalogue :

import { Component } from '@angular/core';
import { EventBusService } from '../shared/event-bus.service';

@Component({
  selector: 'app-product-card',
  template: `
    <div class="card">
      <h3>Produit A</h3>
      <p>Un produit utile et bien conçu.</p>
      <button (click)="addToCart()">Ajouter au panier</button>
    </div>
  `,
})
export class ProductCardComponent {
  constructor(private eventBus: EventBusService) {}

  addToCart() {
    this.eventBus.emit('ADD_TO_CART', { productId: 1, quantity: 1 });
  }
}

Et côté shell :

constructor(private eventBus: EventBusService) {}

ngOnInit() {
  this.eventBus.events$.subscribe(event => {
    if (event.type === 'ADD_TO_CART') {
      console.log('Produit ajouté au panier', event.payload);
    }
  });
}

Cet exemple est volontairement simple, mais il montre l’essentiel : chaque domaine garde son autonomie, tandis que le shell reste capable de coordonner l’ensemble.

Les erreurs fréquentes à éviter

La première erreur est de découper trop tôt. Beaucoup d’équipes veulent absolument aller vers les Micro Frontends alors que leur application n’est pas encore assez complexe. Résultat : elles ajoutent de la lourdeur sans résoudre de vrai problème. La deuxième erreur est de mal définir les frontières. Un Micro Frontend qui dépend trop des autres perd son intérêt. La troisième erreur est de partager trop de logique métier dans des libs communes. On croit gagner du temps, mais on fabrique en réalité des dépendances invisibles. La quatrième erreur est d’ignorer la gouvernance technique. Sans règles communes, les équipes divergent rapidement.

Il faut aussi éviter de sous-estimer l’observabilité. Quand quelque chose casse dans une architecture distribuée, il est indispensable de savoir où, quand et pourquoi. Sans logs centralisés, sans métriques et sans suivi des erreurs, le diagnostic devient pénible. Enfin, il ne faut jamais oublier la simplicité. Une architecture doit rester compréhensible. Si elle devient trop sophistiquée pour l’organisation qui la porte, elle finit par produire l’effet inverse de celui recherché.

Gouvernance technique et conventions

Une architecture Micro Frontend saine repose sur quelques conventions communes. Il faut définir comment nommer les remotes, comment versionner les interfaces, comment partager les bibliothèques, comment gérer les routes, comment déployer les artefacts, comment consigner les erreurs, et comment documenter les contrats. Cette gouvernance ne doit pas être lourde au point de tuer l’autonomie, mais elle doit exister.

Un document d’architecture interne peut suffire à poser les bases. Il peut préciser, par exemple, que chaque Micro Frontend doit exposer un module principal, que les dépendances Angular doivent rester en singleton, que les événements globaux doivent suivre une nomenclature claire, et que les assets partagés doivent passer par une bibliothèque de design. Ce type de règles simplifie énormément l’intégration entre équipes.

Dans les faits, la meilleure gouvernance est souvent celle qui protège la liberté sans autoriser l’improvisation permanente. Il s’agit moins d’imposer que d’aligner.

Évolution progressive depuis un monolithe front-end

Il est rare qu’une entreprise puisse repartir de zéro. Le plus souvent, on part d’un front-end Angular existant, puis on souhaite évoluer progressivement vers une architecture Micro Frontend. C’est parfaitement possible, à condition d’y aller par étapes.

On peut commencer par identifier un premier domaine autonome, puis l’extraire sous forme de remote. Le shell continue d’héberger l’existant, tandis que le nouveau domaine est servi séparément. Une fois cette première migration validée, on peut extraire un second domaine, puis un troisième. Cette approche incrémentale réduit le risque et permet de capitaliser sur l’existant.

Il est même souvent judicieux de ne pas chercher à tout transformer d’un coup. Une migration progressive permet d’apprendre, d’ajuster les conventions et de corriger les erreurs de conception au fil de l’eau. En architecture, la patience est souvent une vertu stratégique.

Le point de vue des équipes

Du point de vue des développeurs, les Micro Frontends peuvent être très motivants. Chaque équipe possède une zone de responsabilité claire, un cycle de livraison plus direct et une plus grande visibilité sur son périmètre. Cela améliore souvent le sentiment d’autonomie et de maîtrise. Mais cette autonomie vient avec une responsabilité supplémentaire : chaque équipe doit penser à l’intégration, à la qualité et à la compatibilité.

Du point de vue du management produit, les Micro Frontends offrent une meilleure capacité à livrer par domaine, à prioriser différemment et à réduire les goulots d’étranglement. Du point de vue de l’exploitation, ils demandent cependant une CI/CD plus robuste, une surveillance plus fine et une stratégie de versioning claire. Du point de vue de l’utilisateur, enfin, tout doit rester simple, cohérent et rapide. Si l’architecture est brillante mais que l’expérience se dégrade, elle échoue sur son objectif principal.

C’est pour cela qu’une bonne architecture n’est jamais purement technique. Elle doit être lisible, soutenable et alignée avec les besoins concrets de l’entreprise.

Exemple de pipeline de déploiement

Même si l’outil exact dépend de votre stack, un pipeline CI/CD pour un Micro Frontend Angular suit souvent cette logique : installation des dépendances, exécution des tests, build du remote, publication des artefacts, mise à jour du manifeste de déploiement, et éventuellement déploiement vers un CDN ou un hébergement statique.

Un pseudo-pipeline pourrait ressembler à ceci :

name: deploy-catalog
on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install
        run: npm ci

      - name: Test
        run: npm run test -- --watch=false

      - name: Build
        run: npm run build:catalog

      - name: Deploy
        run: npm run deploy:catalog

Dans une vraie organisation, ce pipeline doit aussi inclure la gestion des versions, la promotion entre environnements et les contrôles de qualité. Le but est de rendre les livraisons rapides, mais aussi fiables et reproductibles.

Faut-il tout micro-fragmenter ?

Non. C’est sans doute le conseil le plus important de tout cet article. Les Micro Frontends ne sont pas une fin en soi. Leur intérêt dépend du contexte, de la taille du produit, de la structure de l’organisation, du degré d’autonomie souhaité et de la maturité des équipes. Une architecture trop fragmentée peut coûter plus cher qu’elle ne rapporte. Une architecture trop monolithique peut freiner l’évolution. Le bon point d’équilibre n’est jamais universel.

Il faut donc s’autoriser à rester simple quand la simplicité suffit, et à évoluer vers un modèle plus distribué quand le besoin devient réel. L’objectif n’est pas d’adopter une tendance, mais de construire un système qui dure.

Conclusion

Créer une architecture Micro Frontend avec Angular, c’est accepter de penser au-delà du composant et même au-delà du module. C’est réfléchir en domaines, en équipes, en contrats d’intégration, en autonomie de livraison et en expérience utilisateur cohérente. Angular offre un terrain particulièrement favorable à cette approche grâce à sa modularité, son routing, son écosystème et sa compatibilité avec les outils modernes de fédération et de monorepo. Mais la réussite ne dépend pas seulement de la technologie. Elle dépend surtout de la qualité du découpage, de la discipline collective, de la gouvernance technique et du respect de l’utilisateur final.

Une bonne architecture Micro Frontend ne doit pas se voir. Elle doit se faire sentir dans la fluidité du travail des équipes, dans la stabilité des livraisons et dans la qualité de l’expérience produit. Quand elle est bien conçue, elle donne cette impression rare d’un système à la fois indépendant et harmonieux. Chaque équipe avance à son rythme, chaque domaine reste maître de son évolution, et le tout compose un produit cohérent, solide et durable.

#Micro Frontend Angular #architecture Micro Frontend #Angular Module Federation #Angular scalable architecture #front-end modulaire #monorepo Angular #Nx Angular #communication entre micro frontends #lazy loading Angular #déploiement micro frontends

Abonnez-vous à notre newsletter

12k+

Abonnés

Hebdomadaire

Fréquence

Gratuit

Toujours