ITSkillsCenter
Blog

Angular architecture modulaire : organiser une grosse application

12 min de lecture

Lecture : 13 minutes · Niveau : intermédiaire-avancé · Mise à jour : avril 2026

Une application Angular peut atteindre des centaines de composants, des dizaines de services, plusieurs équipes qui contribuent en parallèle. Sans architecture claire, le code devient un labyrinthe où chaque modification touche dix endroits. Ce guide trace les principes qui font qu’une grosse application Angular reste maintenable sur la durée, avec les patterns standalone modernes.

Voir aussi → Angular pour entreprise : guide pratique.


Sommaire

  1. Trois familles de code
  2. Structure feature-first
  3. Lazy loading par route
  4. Communication entre features
  5. Services : où les définir
  6. Bibliothèques partagées et monorepo
  7. Conventions d’équipe
  8. Migration progressive depuis l’ancien style
  9. FAQ

1. Trois familles de code

Une application Angular bien organisée distingue trois familles de code :

Core : services singletons globaux, intercepteurs, guards, modèles de données partagés. Initialisé une seule fois au démarrage. Exemples : AuthService, ApiService, NotificationService, ErrorHandlerService.

Shared : composants, directives, pipes réutilisables sans logique métier propre. Boutons, formulaires génériques, modales, badges, formatters. Importables par toutes les features.

Features : modules métiers fonctionnels. Une feature « clients », une « commandes », une « facturation ». Chacune contient ses composants, services, routes, types — auto-suffisante autant que possible.

Cette séparation, bien que conceptuelle, oriente fortement la structure des dossiers et les dépendances autorisées entre parties du code. Une feature ne doit jamais dépendre d’une autre feature directement (mais peut dépendre de core et shared).


2. Structure feature-first

src/
├── app/
│   ├── core/
│   │   ├── auth/
│   │   │   ├── auth.service.ts
│   │   │   ├── auth.guard.ts
│   │   │   └── auth.interceptor.ts
│   │   ├── api/
│   │   │   └── api.service.ts
│   │   └── error/
│   ├── shared/
│   │   ├── ui/
│   │   │   ├── button/
│   │   │   ├── modal/
│   │   │   └── card/
│   │   ├── pipes/
│   │   └── directives/
│   ├── features/
│   │   ├── clients/
│   │   │   ├── pages/
│   │   │   │   ├── clients-list.component.ts
│   │   │   │   └── client-detail.component.ts
│   │   │   ├── components/
│   │   │   │   ├── client-card.component.ts
│   │   │   │   └── client-form.component.ts
│   │   │   ├── services/
│   │   │   │   └── client.service.ts
│   │   │   ├── models/
│   │   │   │   └── client.model.ts
│   │   │   └── clients.routes.ts
│   │   ├── orders/
│   │   ├── invoices/
│   │   └── dashboard/
│   ├── app.component.ts
│   ├── app.config.ts
│   └── app.routes.ts
├── assets/
├── environments/
└── styles.scss

Cette structure guide l’organisation : chaque feature est un dossier auto-contenu, plus facile à naviguer et à donner à un développeur dédié. Si une feature devient trop grosse, on peut la décomposer en sous-features.

Fichiers d’index

// features/clients/index.ts
export { ClientsListComponent } from "./pages/clients-list.component";
export { ClientService } from "./services/client.service";
export type { Client } from "./models/client.model";

Permet d’importer simplement : import { Client } from "@features/clients".

Path aliases dans tsconfig

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@core/*": ["app/core/*"],
      "@shared/*": ["app/shared/*"],
      "@features/*": ["app/features/*"]
    }
  }
}

Imports plus lisibles que les chemins relatifs profonds.


3. Lazy loading par route

Avec les composants standalone, le lazy loading est naturel. Chaque feature a ses propres routes chargées à la demande.

// app.routes.ts
import { Routes } from "@angular/router";

export const routes: Routes = [
  {
    path: "",
    loadComponent: () => import("@features/dashboard/dashboard.component")
      .then(m => m.DashboardComponent),
  },
  {
    path: "clients",
    loadChildren: () => import("@features/clients/clients.routes")
      .then(m => m.clientsRoutes),
  },
  {
    path: "orders",
    loadChildren: () => import("@features/orders/orders.routes")
      .then(m => m.ordersRoutes),
  },
  { path: "**", redirectTo: "" },
];

// features/clients/clients.routes.ts
export const clientsRoutes: Routes = [
  {
    path: "",
    loadComponent: () => import("./pages/clients-list.component")
      .then(m => m.ClientsListComponent),
  },
  {
    path: ":id",
    loadComponent: () => import("./pages/client-detail.component")
      .then(m => m.ClientDetailComponent),
    resolve: { client: clientResolver },
  },
];

Bénéfices

  • Bundle initial réduit : seul le code de la première page est chargé
  • Builds en parallèle : les bundlers modernes (esbuild) compilent les chunks indépendamment
  • Navigation rapide : les chunks suivants se chargent à la demande
  • Cache navigateur efficace : un chunk inchangé reste en cache même après mise à jour

Préchargement intelligent

Pour anticiper les routes probablement visitées :

import { provideRouter, withPreloading, PreloadAllModules } from "@angular/router";

// Précharge tout après le bootstrap
provideRouter(routes, withPreloading(PreloadAllModules));

// Stratégie custom
class SelectivePreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>) {
    return route.data?.["preload"] ? load() : of(null);
  }
}

Permet de marquer certaines routes comme prioritaires.


4. Communication entre features

Une feature ne devrait pas importer de composant d’une autre feature. Comment alors les faire communiquer ?

Via services partagés (core)

// core/notifications/notification.service.ts
@Injectable({ providedIn: "root" })
export class NotificationService {
  private messages = signal<Message[]>([]);
  list = this.messages.asReadonly();

  notify(msg: string) {
    this.messages.update(list => [...list, { id: crypto.randomUUID(), text: msg }]);
  }
}

Toute feature peut injecter ce service et notifier. Le toaster global (dans app.component) lit les messages et les affiche.

Via routing avec paramètres

Pour passer du contexte entre pages : utiliser les params, query params, ou state du Router.

this.router.navigate(["/orders/new"], {
  queryParams: { clientId: id },
});

// Dans la page cible
clientId = inject(ActivatedRoute).snapshot.queryParamMap.get("clientId");

Via store global pour cas complexes

Pour des états vraiment partagés (utilisateur connecté, panier, préférences UI) : un service core qui expose des signals. NgRx pour les architectures plus formelles, mais pas nécessaire pour la majorité des cas.

Anti-pattern : import direct entre features

// Mauvais : feature A importe un composant de feature B
import { OrderCard } from "@features/orders/components/order-card.component";

// Si OrderCard est vraiment réutilisable, le déplacer dans shared

5. Services : où les définir

Trois niveaux de scope pour les services Angular.

providedIn: "root" — singleton global

@Injectable({ providedIn: "root" })
export class ApiService { ... }

Une seule instance pour toute l’app. Pour les services core utilisés partout : auth, api, logger, notifications.

Provided dans une route lazy

{
  path: "clients",
  loadChildren: () => import("./clients.routes").then(m => m.clientsRoutes),
  providers: [
    ClientService,  // instancié uniquement pour cette feature
  ],
}

Le service n’existe que tant que l’utilisateur est dans la feature. Quand il navigue ailleurs, l’instance est détruite. Adapté pour des services porteurs d’état spécifique à la feature.

Provided au niveau du composant

@Component({
  ...,
  providers: [LocalStateService],
})
export class WizardComponent { ... }

Une instance par composant. Pour des états très locaux (wizards multi-étapes, formulaires complexes).

Recommandation pratique

  • Core services : providedIn: "root"
  • Feature services : provided dans la route racine de la feature (lazy)
  • Component-local services : providers du composant

Cette discipline évite des fuites d’état entre features et clarifie le cycle de vie.


6. Bibliothèques partagées et monorepo

Pour des PME avec plusieurs applications Angular partageant du code commun (design system, services API, types), un monorepo Nx est la solution moderne.

npx create-nx-workspace ma-pme --preset=angular-monorepo

Structure :

ma-pme/
├── apps/
│   ├── admin/
│   ├── client-portal/
│   └── public-site/
└── libs/
    ├── shared/
    │   ├── ui/         # design system
    │   ├── data-access/  # API services partagés
    │   └── util/
    └── domain/
        ├── clients/
        └── orders/

Bénéfices

  • Code partagé sans publication : pas de packages npm à publier
  • Refactoring atomique : un changement qui traverse plusieurs apps est dans un seul commit
  • Build incrémental : Nx ne rebuild que ce qui a changé
  • Linting des dépendances : enforcer des règles entre libs (ex: une lib UI ne dépend jamais d’une lib feature)

Quand un monorepo est-il pertinent ?

  • 2+ applications avec du code commun
  • Une application + plusieurs librairies internes versionnées
  • Équipes multiples qui ont besoin de partager du code

Pour une seule application Angular : pas besoin de monorepo, structure de projet classique suffit.


7. Conventions d’équipe

Une bonne architecture sur le papier ne tient que si l’équipe applique les conventions.

Naming conventions

  • Composants : clients-list.component.tsClientsListComponent
  • Services : client.service.tsClientService
  • Pipes : highlight.pipe.tsHighlightPipe
  • Directives : auto-focus.directive.tsAutoFocusDirective
  • Guards : auth.guard.tsauthGuard (function)
  • Resolvers : client.resolver.tsclientResolver (function)

Imports : règles à enforcer

  • Pas d’import depuis @features/A dans @features/B
  • Pas d’import depuis @features/* dans @core ou @shared
  • Pas d’import depuis @shared dans @core

Avec Nx, des règles de dépendance enforce cela automatiquement (@nx/enforce-module-boundaries). Sans Nx, ESLint avec eslint-plugin-import peut pointer les violations.

Conventions de PR

  • Une feature = une PR autant que possible
  • Composants ciblés (< 250 lignes idéalement)
  • Tests unitaires pour les services critiques
  • Pas de logique métier dans les composants (déléguer aux services)

8. Migration progressive depuis l’ancien style

Une application Angular avec NgModules peut migrer progressivement vers les standalone components.

Étape 1 : standalone components

ng generate @angular/core:standalone

L’outil officiel migre les composants un par un. Choisir l’option « Convert all components, directives and pipes to standalone ».

Étape 2 : remplacer les NgModule par les routes

Les NgModule de routing deviennent des fichiers de routes :

// Avant
@NgModule({
  declarations: [ClientsListComponent, ClientDetailComponent],
  imports: [CommonModule, RouterModule.forChild(routes)],
})
export class ClientsModule {}

// Après
export const clientsRoutes: Routes = [
  { path: "", component: ClientsListComponent },
  { path: ":id", component: ClientDetailComponent },
];

Étape 3 : modernisation progressive

  • Migrer @Input() vers input() au fur et à mesure des modifications
  • Convertir les BehaviorSubject en signals
  • Remplacer *ngIf / *ngFor par @if / @for
  • Adopter les nouvelles fonctions guards/resolvers au lieu des classes

Ne pas chercher à tout moderniser d’un coup. Chaque feature touchée pour une raison métier devient l’occasion de moderniser, sans bloquer les livraisons.

Voir aussi → Angular performance optimisation pour les optimisations qui découlent de cette modernisation.


9. FAQ

Combien de composants par feature avant de découper ?

Indicatif : si une feature dépasse 30-50 composants, elle gagne souvent à être décomposée en sous-features. Pas de règle absolue. Le signal pratique : si trois développeurs ne peuvent plus travailler en parallèle sur la feature sans se gêner, c’est trop gros.

Faut-il un fichier de barrel export index.ts par feature ?

Utile pour exposer l’interface publique d’une feature et cacher l’interne. Mais attention : sur de gros projets, les barrels mal pensés ralentissent le build (Vite, esbuild résolvent le barrel à chaque build). Garder simple ou ne pas en mettre.

Lazy loading systématique ou seulement pour grosses features ?

Systématique pour les features. Pour une seule feature très petite, le lazy loading ajoute un round-trip pour rien, mais ce coût est négligeable. Cohérence > optimisation marginale.

Nx vs Angular CLI pour démarrer ?

Pour une seule app : Angular CLI suffit. Pour 2+ apps avec code commun ou ambition de croissance : Nx vaut l’investissement initial. Migration possible plus tard mais plus coûteuse.

Comment partager des types entre frontend Angular et backend Node.js ?

Soit dans une lib partagée (monorepo Nx), soit en publiant un paquet npm interne. La première option est plus simple pour des PME. Pour le backend en Node.js sans monorepo : copier-coller des types est encore acceptable si peu de types et peu de changements.

Les anciens NgModules doivent-ils tous disparaître ?

À long terme oui, tous les nouveaux projets sont en standalone. Pour les projets existants, migrer progressivement quand c’est rentable. Pas de pression urgente, NgModules restent supportés.

Faut-il un store global type NgRx pour une grosse app ?

Pas obligatoire. Les signals dans des services core couvrent beaucoup de cas avec moins de code. NgRx reste pertinent si l’équipe maîtrise déjà Redux et que les outils NgRx (devtools, actions traçables) sont valorisés.


Articles liés (cluster Angular)


Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité