Angular Best Practices : architecture de projet scalable

🏷️ Front-end 📅 13/04/2026 17:00:00 👤 Mezgani said
Angular Best Practices Architecture Scalable Standalone
Angular Best Practices : architecture de projet scalable

Structurez proprement une application Angular avec core, shared et features. Guide de bonnes pratiques pour une architecture lisible, maintenable et scalable.

Pourquoi structurer son projet

Beaucoup d'applications Angular démarrent proprement puis deviennent difficiles à maintenir après quelques sprints. Le problème ne vient pas du framework, mais d'une architecture qui mélange composants, services, logique métier et intégrations techniques dans les mêmes dossiers.

Une bonne architecture Angular doit répondre à trois objectifs: localiser les responsabilités, réduire le couplage et rendre les évolutions prévisibles. Concrètement, chaque feature doit être compréhensible seule, testable seule et déployable sans casser le reste de l'application.

A retenir: un projet Angular scalable n'est pas organisé par type de fichier uniquement, mais par domaines fonctionnels et responsabilités techniques.

Organisation core shared features

La structure la plus saine en Angular reste un découpage en trois zones principales: core, shared et features.

src/app/
  core/
    auth/
    http/
    layout/
  shared/
    components/
    directives/
    pipes/
    utils/
  features/
    dashboard/
    billing/
    users/
  • core/ contient les services globaux, providers applicatifs, guards, interceptors et layout principal.
  • shared/ contient les briques réutilisables sans dépendance métier forte.
  • features/ contient les écrans, cas d'usage et composants dédiés à un domaine.

Avec les standalone components, ce découpage devient encore plus simple: chaque feature embarque ses routes, ses composants, ses facades et ses services locaux sans passer par des NgModules artificiels.

Separer UI, services et logique metier

Une erreur classique consiste à injecter directement HttpClient dans des composants d'écran et à y traiter toutes les règles métier. Résultat: le composant devient impossible à relire, à tester ou à réutiliser.

Le pattern recommandé est le suivant:

users/
  pages/
    users-page.component.ts
  components/
    users-table.component.ts
  data-access/
    users-api.service.ts
  state/
    users.facade.ts
  models/
    user.model.ts

Le composant de page orchestre, la facade expose un état lisible, le service API parle au backend, et les composants de présentation ne connaissent que des inputs/outputs simples.

Règle utile

Si votre composant contient à la fois des appels HTTP, de la transformation de données et du rendu, il porte trop de responsabilités.

Routage et lazy loading

Le routeur Angular doit suivre la structure fonctionnelle de l'application. Chaque feature importante mérite son point d'entrée dédié avec lazy loading, guards et resolveurs uniquement là où ils apportent une vraie valeur.

export const routes: Routes = [
  {
    path: 'users',
    loadChildren: () => import('./features/users/users.routes')
      .then(m => m.USERS_ROUTES)
  },
  {
    path: 'billing',
    loadChildren: () => import('./features/billing/billing.routes')
      .then(m => m.BILLING_ROUTES)
  }
];

Le lazy loading réduit le coût initial, mais surtout impose des frontières propres entre domaines. C'est une décision d'architecture autant que de performance.

Anti-patterns frequents

  • Un dossier services/ unique avec 40 fichiers sans regroupement métier.
  • Des composants qui font du rendu, du state management et des appels API.
  • Des utilitaires globaux qui deviennent une dépendance cachée partout.
  • Des composants shared trop spécifiques à une seule feature.
  • Une convention de nommage changeante selon les développeurs.

Quand un projet devient illisible, la vraie dette n'est pas le nombre de fichiers, mais l'absence de frontières nettes.

Checklist de production

  • Organiser le projet par features, pas seulement par types de fichiers.
  • Isoler le code applicatif global dans core.
  • Limiter shared aux briques vraiment réutilisables.
  • Utiliser des facades ou services d'orchestration pour éviter les composants trop lourds.
  • Aligner routes, dossiers et responsabilités fonctionnelles.

Si votre arborescence permet de deviner immédiatement où ajouter une nouvelle feature, l'architecture est déjà en bon état.

Architecture avec Standalone Components

Jusqu'à Angular 14, tout composant devait appartenir à un NgModule. Cette contrainte forçait à créer des modules artificiels comme SharedModule ou CoreModule uniquement pour satisfaire le compilateur, sans valeur architecturale réelle. Angular 17 généralise les standalone components comme approche par défaut : plus aucun NgModule n'est nécessaire.

En standalone, chaque composant déclare ses imports directement. La configuration applicative se centralise dans un fichier app.config.ts qui remplace le rôle de l'AppModule :

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './core/auth/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    // Routeur avec preloading de tous les modules lazy
    provideRouter(routes, withPreloading(PreloadAllModules)),
    // HttpClient configuré avec un interceptor fonctionnel
    provideHttpClient(withInterceptors([authInterceptor])),
  ]
};

Chaque feature expose ses routes dans un fichier .routes.ts dédié. Les composants sont chargés en lazy loading individuel grâce à loadComponent :

// features/users/users.routes.ts
import { Routes } from '@angular/router';

export const USERS_ROUTES: Routes = [
  {
    path: '',
    // Lazy loading du composant de liste
    loadComponent: () => import('./pages/users-page.component')
      .then(m => m.UsersPageComponent)
  },
  {
    path: ':id',
    // Lazy loading du composant de détail
    loadComponent: () => import('./pages/user-detail.component')
      .then(m => m.UserDetailComponent)
  }
];
Avantage clé : Avec les standalone components, chaque feature peut être chargée de manière totalement isolée. Plus besoin d'un SharedModule : chaque composant importe uniquement ce dont il a besoin, ce qui réduit les dépendances implicites et accélère les builds.

Conventions de nommage et barrel files

Une convention de nommage cohérente est la première ligne de défense contre la dette technique. Angular impose des suffixes officiels qui permettent d'identifier le rôle d'un fichier avant même de l'ouvrir. Les respecter sur tout le projet évite des ambiguïtés coûteuses en revue de code.

Type Suffixe de fichier Exemple
Composant .component.ts users-table.component.ts
Service .service.ts users-api.service.ts
Directive .directive.ts auto-focus.directive.ts
Pipe .pipe.ts truncate.pipe.ts
Guard .guard.ts auth.guard.ts
Interceptor .interceptor.ts auth.interceptor.ts
Modèle .model.ts user.model.ts
Routes .routes.ts users.routes.ts

Les barrel files (index.ts) permettent de définir l'API publique d'une feature. Plutôt qu'exposer l'intégralité de l'arborescence interne, on choisit explicitement ce qui est accessible depuis l'extérieur :

// features/users/index.ts — API publique de la feature
export { USERS_ROUTES } from './users.routes';
export type { User } from './models/user.model';

Les autres features peuvent ainsi importer depuis './features/users' sans connaître la structure interne du dossier. Si l'organisation interne change, un seul fichier est à mettre à jour.

Règle d'isolation

Ne jamais importer directement depuis les internals d'une autre feature (ex: import ... from './features/users/models/user.model'). Toujours passer par le barrel file index.ts. Cette règle préserve l'encapsulation et évite les couplages invisibles entre domaines.