AngularAtlaséd. 22.2

Chap. 13Architecture à grande échelle60 / 75

Architecture 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 ~0 min de lecture

L’architecture hexagonale (ou ports & adaptateurs) part d’une idée simple : les règles de l’application n’ont pas besoin de savoir d’où viennent les données ni comment elles sont affichées. Le domaine est au centre, écrit en TypeScript pur. Autour, les cas d’usage ne parlent au monde extérieur qu’à travers des ports, c’est-à-dire des contrats. En périphérie se trouvent les adaptateurs : HTTP, localStorage, composants Angular. Vous pouvez les remplacer sans toucher au centre.

Dans Angular, l’idée s’accorde très bien avec la DI : une classe abstraite est à la fois le type du port et le token d’injection. { provide: TaskRepository, useClass: HttpTaskRepository } est tout le « câblage » dont vous avez besoin.

Pourquoi c’est important
Quand le backend change (autre API, GraphQL, offline-first), vous ne modifiez qu’un seul adaptateur. Quand vous testez les règles, vous ne démarrez pas HttpClient. Et la logique métier ne se disperse plus dans les composants, où elle est difficile à trouver et à réutiliser.

Les couches et la règle de dépendance

Il y a quatre couches, et les imports ne pointent que vers l’intérieur. Le domaine n’importe rien d’extérieur. L’application connaît le domaine et les ports, mais pas les adaptateurs. Les adaptateurs implémentent les ports. Le seul endroit qui lie un port à un adaptateur concret est la composition root : app.config.ts, ou les providers d’une route.

                 ┌──────────────────────── extérieur : adaptateurs ────────────────────┐
                 │  presentation (driving)                 infrastructure (driven)     │
  utilisateur ─▶ │  TasksPage, TaskItem                    HttpTaskRepository, TaskDto │ ──▶ server
                 │        │   ┌──────────── application ─────────────┐   ▲             │
                 │        └──▶│ TaskBoard / AddTask ──▶ TaskRepository│───┘ implémente   │
                 │            │        │      ┌──── domain ────┐     │                  │
                 │            │        └─────▶│ Task, createTask│    │                  │
                 │            │               └─────────────────┘     │                  │
                 │            └───────────────────────────────────────┘                  │
                 └──────────────────────────────────────────────────────────────────────┘
  Règle : les flèches d’import pointent UNIQUEMENT vers l’intérieur. Le domaine n’importe rien de l’extérieur.
  app.config.ts (composition root) est le seul endroit qui lie TaskRepository à un adaptateur.
CoucheContientPeut importer
domainentités, règles, erreurs du domainerien (uniquement TypeScript)
applicationports (classes abstraites), cas d’usage, état pour l’UIdomain, @angular/core (DI, Signals)
infrastructureadaptateurs : HTTP, stockage, DTO, mappingapplication (les ports), domain, HttpClient
presentationcomposants smart et dumbapplication, domain (types) — jamais infrastructure
LiveRègle de dépendance : cet import est-il autorisé ?

1. Choisissez le fichier qui fait l’import.

presentation · adaptateurs pilotants
infrastructure · adaptateurs pilotés
application · ports + cas d’usage
domain · TypeScript pur
framework & browser
composition root
Exemples :
Choisissez d’où vient l’import et ce qu’il importe. Le verdict vous dit si la règle est respectée et ce qu’il faut faire à la place.

Domain : TypeScript pur

Le domaine contient les entités et les règles : « un titre a au moins 3 caractères », « deux tâches ouvertes ne peuvent pas avoir le même titre ». Il n’importe ni Angular, ni RxJS, ni les API du navigateur. Même l’horloge et la génération d’id arrivent en paramètres, pour que les règles restent déterministes. Le gain principal : le domaine se teste avec de simples fonctions, sans TestBed et sans mocks.

// tasks/domain/task.ts — TypeScript pur : aucun import d’Angular, de RxJS ni du navigateur
export type TaskId = string & { readonly __brand: 'TaskId' };

export interface Task {
  readonly id: TaskId;
  readonly title: string;
  readonly done: boolean;
  readonly createdAt: Date;
}

export class DomainError extends Error {
  override readonly name = 'DomainError';
}

export function normalizeTitle(raw: string): string {
  return raw.trim().replace(/\s+/g, ' ');
}

// `now` et `id` arrivent en paramètres : le domaine ne lit pas l’horloge et ne génère pas d’ids lui-même
export function createTask(raw: string, existing: readonly Task[], now: Date, id: TaskId): Task {
  const title = normalizeTitle(raw);
  if (title.length < 3) throw new DomainError('Le titre doit contenir au moins 3 caractères.');
  if (existing.some((t) => !t.done && t.title.toLowerCase() === title.toLowerCase())) {
    throw new DomainError('Il existe déjà une tâche ouverte avec ce titre.');
  }
  return { id, title, done: false, createdAt: now };
}

export function toggleTask(task: Task): Task {
  return { ...task, done: !task.done };
}

Ports : des classes abstraites comme tokens

Un port décrit ce dont l’application a besoin (« lister, enregistrer, supprimer »), pas comment. Vous l’écrivez comme une classe abstraite : elle existe à l’exécution, donc inject(TaskRepository) fonctionne directement. Une interface disparaîtrait à la compilation, et il vous faudrait un InjectionToken séparé (voir InjectionToken). Les erreurs font partie du contrat : les adaptateurs traduisent leurs erreurs techniques (timeout, 503) en RepositoryUnavailableError, pour que l’UI ne dépende pas de HttpErrorResponse.

// tasks/application/task-repository.ts — le PORT
import { Task, TaskId } from '../domain/task';

// Classe abstraite, pas une interface : elle existe à l’exécution, c’est donc directement un token de DI.
// Elle n’a pas de @Injectable et ne connaît aucune implémentation.
export abstract class TaskRepository {
  abstract list(): Promise<readonly Task[]>;
  abstract save(task: Task): Promise<void>; // upsert
  abstract remove(id: TaskId): Promise<void>;
}

// Fait partie du contrat : les adaptateurs y traduisent leurs erreurs techniques.
export class RepositoryUnavailableError extends Error {
  override readonly name = 'RepositoryUnavailableError';
}

Cas d’usage

Un cas d’usage orchestre : il lit via un port, applique les règles du domaine, puis enregistre via un port. Dans Angular, c’est un service ordinaire qui injecte le port, pas l’adaptateur. Pour l’UI, un « board » qui garde l’état dans des Signals (tasks, error, open) est pratique. Les composants ne font que le lire, exactement comme dans State Management.

// tasks/application/add-task.ts — un cas d’usage = une classe avec une seule méthode publique
import { Injectable, inject } from '@angular/core';
import { Task, TaskId, createTask } from '../domain/task';
import { TaskRepository } from './task-repository';

@Injectable({ providedIn: 'root' })
export class AddTask {
  private readonly repo = inject(TaskRepository); // le port — ne sait pas quel adaptateur répond

  async execute(title: string): Promise<Task> {
    const existing = await this.repo.list();
    const task = createTask(title, existing, new Date(), crypto.randomUUID() as TaskId); // la règle : dans le domaine
    await this.repo.save(task); // E/S : via le port
    return task;
  }
}

Adaptateurs

Un adaptateur extends TaskRepository et fait toutes les E/S : HTTP, localStorage, mémoire. Le mapping DTO → domaine vit ici aussi (is_done: 1 devient done: true, la chaîne ISO devient une Date ; voir Interfaces et modèles). Le format du serveur ne dépasse pas l’adaptateur.

// tasks/infrastructure/http-task-repository.ts — l’ADAPTATEUR HTTP
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable, firstValueFrom } from 'rxjs';
import { Task, TaskId } from '../domain/task';
import { RepositoryUnavailableError, TaskRepository } from '../application/task-repository';
import { TaskDto, taskFromDto, taskToDto } from './task-dto';

@Injectable()
export class HttpTaskRepository extends TaskRepository {
  private readonly http = inject(HttpClient);
  private readonly url = '/api/tasks';

  async list(): Promise<readonly Task[]> {
    const dtos = await this.call(this.http.get<TaskDto[]>(this.url));
    return dtos.map(taskFromDto); // DTO → domaine, ici et nulle part ailleurs
  }

  async save(task: Task): Promise<void> {
    await this.call(this.http.put<TaskDto>(`${this.url}/${task.id}`, taskToDto(task)));
  }

  async remove(id: TaskId): Promise<void> {
    await this.call(this.http.delete<void>(`${this.url}/${id}`));
  }

  // HttpErrorResponse ne quitte pas l’adaptateur : le cœur reçoit l’erreur du port
  private async call<T>(request: Observable<T>): Promise<T> {
    try {
      return await firstValueFrom(request);
    } catch (e) {
      if (e instanceof HttpErrorResponse) throw new RepositoryUnavailableError(`HTTP ${e.status}`);
      throw e;
    }
  }
}
LiveLa même UI, trois adaptateurs

{ provide: TaskRepository, useClass: InMemoryTaskRepository } — Une Map en mémoire — les données disparaissent quand l’injecteur est détruit.

UI identique : TasksPanel → TaskBoard → TaskRepository
  • Aucune tâche.
0 ouvertes sur 0 terminé
Qui a répondu
  • 11:33.2DI TaskRepository → InMemoryTaskRepository
  • 11:33.2InMemoryTaskRepository list() → 0 (Map)
Changez d’adaptateur et ajoutez des tâches. Le composant et les cas d’usage sont identiques ; le journal indique quel adaptateur a répondu à chaque appel. Avec HTTP, vous pouvez mettre le serveur hors ligne.

Composition root : là où vous choisissez l’adaptateur

La liaison port → adaptateur se trouve à un seul endroit. Globalement, dans app.config.ts. Localement, dans les providers d’une route, où l’adaptateur vit aussi longtemps que la route (voir Providers et hiérarchie des injecteurs). Pour garder privés les détails de la feature, celle-ci expose une fonction provideTasks(). La composition root choisit seulement « http » ou « local ».

// app.config.ts — COMPOSITION ROOT : le seul fichier qui sait quel adaptateur on utilise
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])),
    { provide: TaskRepository, useClass: HttpTaskRepository },
  ],
};

Tests : c’est ici que le gain se voit

Vous avez trois niveaux de tests, tous peu coûteux :

  • le domaine, avec des fonctions pures ;
  • les cas d’usage, avec l’adaptateur en mémoire à la place de HTTP ;
  • les adaptateurs, avec un test de contrat : la même suite exécutée sur chaque implémentation du port.

Le test de contrat attrape exactement les bugs qui passeraient sinon : un adaptateur qui renvoie des DTO au lieu d’entités, ou un autre qui duplique au lieu de mettre à jour.

// Test de cas d’usage : l’adaptateur en mémoire à la place de HTTP
import { TestBed } from '@angular/core/testing';

describe('AddTask', () => {
  let addTask: AddTask;
  let repo: InMemoryTaskRepository;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [{ provide: TaskRepository, useClass: InMemoryTaskRepository }],
    });
    addTask = TestBed.inject(AddTask);
    repo = TestBed.inject(TaskRepository) as InMemoryTaskRepository;
  });

  it('enregistre via le port', async () => {
    await addTask.execute('Écrire des tests');
    expect((await repo.list()).map((t) => t.title)).toEqual(['Écrire des tests']);
  });

  it('n’enregistre rien si une règle du domaine est violée', async () => {
    await expect(addTask.execute('ab')).rejects.toThrow(DomainError);
    expect(await repo.list()).toEqual([]);
  });
});
LiveLe test de contrat, exécuté sur chaque adaptateur
6 tests × 4 adaptateurs
Test (le même pour tous) En mémoirelocalStorageHTTPHTTP sans mapping
un nouveau repository est vide····
save() puis list() renvoie la tâche····
save() avec le même id met à jour, sans dupliquer····
remove() ne supprime que la tâche demandée····
done est un booléen, createdAt est une Date····
renvoie des entités du domaine, pas des DTO····
Lancez la suite : les mêmes tests passent sur l’adaptateur en mémoire, localStorage et HTTP, mais échouent sur l’adaptateur « HTTP sans mapping », qui laisse fuiter les DTO dans le domaine.

Comment faire respecter la règle en équipe

Une règle d’architecture qui n’existe que dans votre tête ne survit pas à la première deadline. Mettez-la dans le lint. Avec no-restricted-imports d’ESLint, découpé par couche, tout mauvais import fait échouer la CI. Dans un monorepo Nx, vous faites la même chose avec des tags et @nx/enforce-module-boundaries (voir Monorepo, bibliothèques et frontières et Barrels, alias et frontières).

// La règle de dépendance, vérifiée à chaque commit (no-restricted-imports d’ESLint avec regex)
export default [
  {
    files: ['src/app/features/*/domain/**/*.ts'],
    rules: {
      'no-restricted-imports': ['error', { patterns: [
        { regex: '^(@angular/|rxjs)', message: 'Le domaine est du TypeScript pur : pas d’Angular, pas de RxJS.' },
        { regex: '/(application|infrastructure|ui)/', message: 'Le domaine ne dépend pas des couches externes.' },
      ] }],
    },
  },
  {
    files: ['src/app/features/*/application/**/*.ts'],
    rules: {
      'no-restricted-imports': ['error', { patterns: [
        { regex: '/(infrastructure|ui)/', message: 'Application dépend des ports, pas des adaptateurs ni de l’UI.' },
        { regex: '^@angular/common/http', message: 'HTTP uniquement dans infrastructure.' },
      ] }],
    },
  },
  {
    files: ['src/app/features/*/ui/**/*.ts'],
    rules: {
      'no-restricted-imports': ['error', { patterns: [
        { regex: '/infrastructure/', message: 'L’UI injecte le port ou le cas d’usage, pas l’adaptateur.' },
      ] }],
    },
  },
];
// Dans un monorepo Nx : domain/application/infrastructure comme bibliothèques séparées, avec des tags
// et @nx/enforce-module-boundaries (voir la page Monorepo).

Quand OUI, quand NON

Les ports et adaptateurs valent le coup
  • De vraies règles métier (prix, validations, flux) qui doivent être testées et réutilisées.
  • Un backend instable ou changeant : une nouvelle API, une migration REST → GraphQL, un mode hors ligne.
  • La même logique sur plusieurs sources de données (serveur, cache local, un mock pour les démos).
  • De grandes applications durables, avec plusieurs équipes sur la même feature.
C’est trop — restez simple
  • Des écrans CRUD qui affichent simplement ce que renvoie le serveur. Un service avec httpResource suffit.
  • Prototypes et petites applications : quatre couches pour deux formulaires ralentissent l’équipe.
  • Quand le « port » n’a qu’une seule implémentation et que vous ne la testerez pas séparément. L’abstraction n’apporte rien.
  • La logique d’UI pure (survol, déploiement) : elle reste dans le composant.

Une variante pragmatique convient à la plupart des projets : un domaine pur pour les règles qui comptent, un port seulement là où vous avez, ou aurez, plusieurs implémentations, et le reste dans la structure habituelle data-access / ui (voir Structure des dossiers).

Pièges fréquents

Le port qui importe l’adaptateur
@Injectable({ providedIn: 'root', useClass: HttpTaskRepository }) sur la classe abstraite paraît pratique, mais le port finit par dépendre de l’infrastructure, et HttpClient atterrit dans le bundle même là où vous ne l’utilisez pas. Le choix de l’adaptateur appartient à la composition root.
// ✗ « Implémentation par défaut » placée sur le port
@Injectable({ providedIn: 'root', useClass: HttpTaskRepository }) // le port importe l’adaptateur !
export abstract class TaskRepository { /* ... */ }
// Application → infrastructure : la règle de dépendance est violée, et HttpClient
// finit dans le bundle même pour l’application qui n’utilise que localStorage.

// ✓ Le port reste vide ; le choix vit dans la composition root
export abstract class TaskRepository { /* ... */ }
providers: [{ provide: TaskRepository, useClass: HttpTaskRepository }]
  • Des DTO qui fuient dans le domaine. Si le type du serveur atteint les composants, tout changement d’API se propage partout. Le mapping vit dans l’adaptateur.
  • Des composants qui injectent l’adaptateur concret (inject(HttpTaskRepository)). Dans ce cas, vous n’avez plus de port du tout. Injectez le port ou le cas d’usage.
  • Un domaine « anémique ». Des entités sans règles, avec toute la logique dans des services : vous avez les couches, mais pas le bénéfice.
  • Des erreurs techniques qui atteignent l’UI. Un HttpErrorResponse dans un composant signifie que l’UI sait qu’il s’agit de HTTP. Traduisez les erreurs dans l’adaptateur.
  • Une horloge et des ids lus dans le domaine (new Date(), crypto.randomUUID()). Les règles deviennent difficiles à tester. Passez-les en paramètres.

Aide-mémoire

COUCHES (les imports ne pointent que vers l’intérieur)
  domain          Task, createTask()         TypeScript pur — ni Angular, ni RxJS, ni navigateur
  application     TaskRepository (port)      classe abstraite = type + token de DI
                  AddTask / TaskBoard        inject(TaskRepository), Signals pour l’UI
  infrastructure  HttpTaskRepository         extends TaskRepository, DTO ↔ domaine, erreurs traduites
  presentation    TasksPage, TaskItem        injecte application, jamais infrastructure
  composition     app.config / route.providers  { provide: TaskRepository, useClass: Http… }

RECETTES
  changer d’adaptateur     un autre useClass dans la composition root (ou provideTasks('local'))
  test de cas d’usage      providers: [{ provide: TaskRepository, useClass: InMemoryTaskRepository }]
  test d’adaptateur        contrat partagé + HttpTestingController pour le mapping HTTP
  DTO                      uniquement dans infrastructure ; le port parle en entités
  erreurs                  l’adaptateur lance RepositoryUnavailableError, pas HttpErrorResponse

QUAND S’EN ABSTENIR
  CRUD sans règles, petite application, prototype → httpResource dans le composant + mapping dans parse

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