<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Architecture angulaire

Créer une application Angular qui fonctionne est relativement simple. Construire une application Angular qui reste ordonnée, lisible, évolutive et facile à maintenir après des mois ou des années de développement est une tout autre affaire. La vraie différence entre un projet amateur et un projet professionnel ne réside pas seulement dans le code qui « fait son travail », mais dans l'architecture qui permet à l'équipe de continuer à se développer sans créer le chaos.

Lorsqu'une application grandit, les pages, les composants, les services, les appels HTTP, les états à gérer, les autorisations, les routes, les formulaires, les validations et les dépendances entre les différentes parties du système augmentent. Si tout est placé dans des dossiers génériques comme components, services et models, après un court laps de temps, il devient difficile de comprendre où se trouve une fonctionnalité, qui utilise quoi et quelles parties du code peuvent être modifiées sans casser le reste.

Dans ce guide, nous voyons comment structurer une application Angular moderne en utilisant une architecture basée sur les fonctionnalités, une séparation claire entre core et shared, les composants autonomes, les signaux, le modèle composants intelligents vs stupides et une structure de dossiers conçue pour de vrais projets.


Pourquoi l'architecture est importante dans Angular

Angular est un framework très puissant car il propose déjà une structure claire : composants, services, injection de dépendances, routage, formulaires, client HTTP, garde, intercepteur et bien plus encore. Cependant, précisément parce qu'Angular propose de nombreux outils, il est facile de les utiliser sans stratégie cohérente.

Un petit projet peut survivre même avec une structure moins ordonnée. Un projet de taille moyenne ou grande a cependant besoin de règles. Sans règles, chaque développeur organise le code à sa manière, les composants deviennent trop volumineux, les services accumulent des responsabilités sans rapport et le projet se transforme peu à peu en un bloc difficile à modifier.

Une bonne architecture angulaire doit permettre d'atteindre certains objectifs fondamentaux :

  • Évolutivité : ajoutez de nouvelles fonctionnalités sans avoir à réorganiser l'ensemble du projet.
  • Maintenabilité : Comprenez rapidement où se trouve le code et comment le modifier.
  • Séparation des responsabilités : Chaque dossier, composant ou service doit avoir un rôle clair.
  • Testabilité : le code doit être facile à tester de manière isolée.
  • Performance : la structure doit privilégier le chargement paresseux, les bundles plus petits et le rendu efficace.
  • Collaboration : Plusieurs développeurs doivent pouvoir travailler sur la même application sans se marcher sur les pieds.

Le point central est le suivant : l’architecture ne sert pas à compliquer le projet, mais à le rendre plus prévisible. Lorsqu'une structure est prévisible, chaque nouvelle caractéristique a un lieu de vie naturel.


Architecture basée sur les fonctionnalités : organiser le code par fonctionnalité

L'une des erreurs les plus courantes dans les projets Angular est d'organiser le code par type technique plutôt que par domaine fonctionnel. Une structure comme celle-ci peut sembler ordonnée au premier abord :

src/app/
  components/
  services/
  models/
  pipes/
  directives/
  pages/

Le problème est que cette organisation ne dit rien sur le produit. Si vous travaillez dans la section projets, vous devrez rechercher des composants dans components, des services dans services, des modèles dans models, des pages dans pages, etc. Chaque fonctionnalité est dispersée tout au long du projet.

Une architecture basée sur les fonctionnalités, quant à elle, organise le code autour des fonctionnalités de l'application. Par exemple :

src/app/
  features/
    dashboard/
    blog/
    projects/
    experiences/
    auth/
    admin/

Chaque dossier représente une partie réelle du produit. Tout ce qui concerne le blog se trouve dans features/blog. Tout ce qui concerne les projets se trouve dans features/projects. Tout ce qui concerne l'administrateur se trouve dans features/admin.

Cette approche améliore grandement la lisibilité du projet. Lorsque vous devez modifier une fonctionnalité, vous savez où aller. Lorsque vous devez supprimer une fonctionnalité, vous savez quels fichiers sont concernés. Lorsqu'un nouveau développeur rejoint l'équipe, il peut comprendre l'application à partir des fonctionnalités et non à partir de dossiers techniques génériques.

Une fonctionnalité doit être la plus autonome possible

Une bonne fonctionnalité Angular doit contenir tout ce dont elle a besoin pour fonctionner : des pages, des composants spécifiques, des services d'accès aux données, des modèles, des magasins locaux, des itinéraires et des utilitaires internes.

Exemple de structure pour une fonctionnalité projets :

src/app/features/projects/
  projects.routes.ts
  pages/
    projects-list-page.component.ts
    project-detail-page.component.ts
  components/
    project-card.component.ts
    project-filters.component.ts
    project-empty-state.component.ts
  data-access/
    projects-api.service.ts
    projects-store.service.ts
  models/
    project.model.ts
    project-filter.model.ts
  utils/
    project-status.util.ts

Cette structure rend la fonctionnalité indépendante et facile à comprendre. Les pages sont séparées par des composants plus petits, la logique d'accès aux données se trouve dans data-access, les types TypeScript sont dans models et les fonctions d'assistance spécifiques aux fonctionnalités sont dans utils.

Règle importante : évitez les dépendances entre fonctionnalités

Une fonctionnalité ne doit pas importer directement des composants, des services ou des modèles à partir d'une autre fonctionnalité. Par exemple, features/blog ne doit pas importer le code de features/projects. Cela crée un couplage et rend difficile la modification d'une fonctionnalité sans affecter les autres.

Si deux fonctionnalités nécessitent le même composant ou utilitaire, cet élément doit probablement être déplacé vers shared. Cependant, s'ils partagent une logique de domaine importante, il peut être utile de créer un dossier ou une bibliothèque dédié, mais toujours avec des limites claires.


Core et Shared : différence fondamentale

Dans de nombreux projets Angular, nous trouvons les dossiers core et shared, mais ils sont souvent mal utilisés. Comprendre la différence entre ces deux domaines est essentiel pour garder votre projet propre.

Core : ce qui appartient à l'application globale

Le dossier core contient du code global, utilisé au niveau de l'application, souvent initialisé une seule fois. Nous trouvons ici des éléments tels que l'authentification, les intercepteurs HTTP, les gardes globaux, la disposition principale, la gestion des erreurs, les configurations, les services singleton et la logique d'infrastructure.

Exemple :

src/app/core/
  auth/
    auth.service.ts
    auth.guard.ts
    auth.interceptor.ts
  http/
    api-error.interceptor.ts
    http-context.tokens.ts
  layout/
    main-layout.component.ts
    admin-layout.component.ts
  config/
    app-config.token.ts
  guards/
    role.guard.ts
  services/
    logger.service.ts
    storage.service.ts

Le core ne doit pas devenir un dépotoir pour des services aléatoires. Il ne doit contenir que ce qui est véritablement mondial. Si un service ne sert que la fonctionnalité de blog, il n'entre pas dans core : il entre dans features/blog/data-access.

Partagé : ce qui est réutilisable et sans logique métier spécifique

Le dossier shared contient des éléments pouvant être réutilisés dans plusieurs parties de l'application, mais non liés à une fonctionnalité spécifique. Nous trouvons ici des composants génériques d'interface utilisateur, des tuyaux, des directives, des assistants et des modèles véritablement partagés.

src/app/shared/
  ui/
    button/
    modal/
    card/
    badge/
    spinner/
  pipes/
    truncate.pipe.ts
    safe-html.pipe.ts
  directives/
    autofocus.directive.ts
    click-outside.directive.ts
  utils/
    date-format.util.ts
    string.util.ts
  models/
    pagination.model.ts
    api-response.model.ts

Un composant tel que app-button, app-modal ou app-spinner peut s'insérer dans shared/ui. Un composant tel que project-card ne doit cependant pas être partagé, car il appartient au domaine des projets.

Erreur courante : tout mettre en Partagé

L'un des anti-modèles les plus fréquents consiste à créer un énorme dossier shared qui contient tout. Cela semble pratique au début, mais après quelques mois, il devient impossible de comprendre quels composants sont véritablement génériques et lesquels ont été placés là uniquement par commodité.

La règle générale est simple : si un composant contient des mots, de la logique ou des concepts liés à une fonctionnalité spécifique, il n'est pas partagé. Si toutefois il est générique, réutilisable et indépendant du domaine, il peut devenir partagé.


Composants autonomes : Angular moderne sans NgModules inutiles

Avec Angular moderne, les composants autonomes sont devenus le moyen recommandé pour créer des applications plus simples, plus explicites et modulaires. Auparavant, chaque composant devait être déclaré dans un NgModule. Cela aboutissait souvent à des formes très volumineuses ou peu claires.

Avec les composants autonomes, chaque composant déclare directement ses dépendances via la propriété imports. Cela rend le code plus lisible : en regardant un composant, vous pouvez immédiatement comprendre quels autres composants, directives ou canaux il utilise.

@Component({
  selector: 'app-project-card',
  standalone: true,
  imports: [DatePipe],
  template: `
    <article class="project-card">
      <h3>{{ project().title }}</h3>
      <p>{{ project().description }}</p>
      <small>Pubblicato il {{ project().createdAt | date }}</small>
      <button type="button" (click)="open.emit(project().id)">
        Apri progetto
      </button>
    </article>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectCardComponent {
  project = input.required<Project>();
  open = output<string>();
}

Cette approche présente plusieurs avantages :

  • les dépendances sont explicites ;
  • les composants sont plus faciles à déplacer et à tester ;
  • le chargement paresseux devient plus naturel ;
  • le besoin de modules intermédiaires est réduit ;
  • le projet semble plus léger mentalement.

Amorçage de l'application avec autonome

Dans une application Angular moderne, le bootstrapping peut être géré sans un AppModule traditionnel. La configuration globale est souvent définie dans app.config.ts.

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
    provideHttpClient(withInterceptors([
      authInterceptor,
      apiErrorInterceptor
    ]))
  ]
};

Et dans le fichier main.ts:

bootstrapApplication(AppComponent, appConfig)
  .catch((error) => console.error(error));

Cette configuration sépare clairement le point de lancement de l'application de la logique des fonctionnalités.


Routage et chargement différé des fonctionnalités

Le routage est un élément central de l'architecture angulaire. Dans un projet évolutif, chaque fonctionnalité doit avoir ses propres itinéraires, chargés paresseux lorsque cela est possible.

Dans le fichier principal app.routes.ts nous ne pouvons définir que les routes principales :

export const routes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./features/home/pages/home-page.component')
        .then((m) => m.HomePageComponent)
  },
  {
    path: 'projects',
    loadChildren: () =>
      import('./features/projects/projects.routes')
        .then((m) => m.PROJECTS_ROUTES)
  },
  {
    path: 'blog',
    loadChildren: () =>
      import('./features/blog/blog.routes')
        .then((m) => m.BLOG_ROUTES)
  },
  {
    path: 'admin',
    canMatch: [authGuard],
    loadChildren: () =>
      import('./features/admin/admin.routes')
        .then((m) => m.ADMIN_ROUTES)
  }
];

À l'intérieur de la fonctionnalité projets, cependant, nous définissons les itinéraires spécifiques :

export const PROJECTS_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./pages/projects-list-page.component')
        .then((m) => m.ProjectsListPageComponent)
  },
  {
    path: ':id',
    loadComponent: () =>
      import('./pages/project-detail-page.component')
        .then((m) => m.ProjectDetailPageComponent)
  }
];

Cette approche maintient le fichier de routes principal propre et permet à Angular de charger uniquement le code nécessaire à la section visitée par l'utilisateur.


Signaux : une gestion des états plus simple et plus réactive

Les signaux sont l'un des outils les plus importants de l'Angular moderne. Ils vous permettent de gérer l'état local et dérivé d'une manière plus simple, plus lisible et plus efficace que de nombreuses solutions traditionnelles.

Un signal représente une valeur réactive. Lorsque la valeur change, Angular sait quelles parties de l'interface utilisateur doivent être mises à jour. Cela rend l'état plus explicite et réduit la complexité dans de nombreux scénarios.

Exemple simple :

const count = signal(0);

const double = computed(() => count() * 2);

function increment(): void {
  count.update((value) => value + 1);
}

Dans une application évolutive, les signaux peuvent être utilisés à différents niveaux :

  • état local du composant, comme onglet actif, filtres, ouverture d'un modal ;
  • état dérivé, en tant qu'éléments totaux, éléments filtrés, état vide ;
  • feature store, lorsqu'une section de l'application contient des données partagées entre plusieurs composants ;
  • facade, pour exposer un état simple et prêt à l'emploi à l'interface utilisateur.

Exemple de magasin avec signaux

Une bonne pratique consiste à créer un magasin spécifique pour la fonctionnalité, en évitant de mettre toute la logique à l'intérieur des composants.

@Injectable()
export class ProjectsStore {
  private readonly api = inject(ProjectsApiService);

  private readonly _projects = signal<Project[]>([]);
  private readonly _loading = signal(false);
  private readonly _error = signal<string | null>(null);
  private readonly _query = signal('');

  readonly projects = this._projects.asReadonly();
  readonly loading = this._loading.asReadonly();
  readonly error = this._error.asReadonly();
  readonly query = this._query.asReadonly();

  readonly filteredProjects = computed(() => {
    const query = this._query().toLowerCase().trim();

    if (!query) {
      return this._projects();
    }

    return this._projects().filter((project) =>
      project.title.toLowerCase().includes(query)
    );
  });

  readonly total = computed(() => this.filteredProjects().length);

  async loadProjects(): Promise<void> {
    this._loading.set(true);
    this._error.set(null);

    try {
      const projects = await firstValueFrom(this.api.getProjects());
      this._projects.set(projects);
    } catch {
      this._error.set('Impossibile caricare i progetti.');
    } finally {
      this._loading.set(false);
    }
  }

  setQuery(query: string): void {
    this._query.set(query);
  }
}

Ce magasin a une responsabilité claire : gérer le statut de la fonctionnalité des projets. Le composant n'a pas besoin de savoir comment les données sont chargées ni comment elles sont filtrées. Il lui suffit de lire les signaux et les méthodes d'appel exposées par le magasin.

Où fournir un magasin de fonctionnalités

Si le magasin ne dessert qu'une fonctionnalité spécifique, celle-ci peut être fournie au niveau de l'itinéraire ou du composant. De cette manière, son cycle de vie est lié à la fonctionnalité elle-même et ne reste pas inutilement global.

export const PROJECTS_ROUTES: Routes = [
  {
    path: '',
    providers: [ProjectsStore, ProjectsApiService],
    loadComponent: () =>
      import('./pages/projects-list-page.component')
        .then((m) => m.ProjectsListPageComponent)
  }
];

Ce choix évite de remplir l'injecteur racine avec des services qui n'ont pas besoin de fonctionner pendant toute la durée de vie de l'application.

Signaux et RxJS : ce ne sont pas des ennemis

Une erreur courante est de penser que les signaux remplacent complètement RxJS. En réalité, les deux outils peuvent très bien cohabiter. Les signaux sont parfaits pour l'état synchrone, les dérivations et l'état de l'interface utilisateur. RxJS reste très utile pour les flux asynchrones, les événements complexes, les WebSockets, l'anti-rebond, les nouvelles tentatives, les combinaisons de flux et l'annulation automatique des requêtes.

Une règle empirique utile :

  • utilise signaux pour représenter l'état actuel de l'interface utilisateur ;
  • utilisez calculé pour les valeurs dérivées ;
  • utilisez RxJS lorsque vous devez modéliser des flux asynchrones dans le temps ;
  • utilisez des conversions telles que toSignal lorsque vous souhaitez exposer plus facilement un observable au modèle.

Composants intelligents ou stupides

Le modèle composants intelligents vs stupides est l'un des plus utiles pour maintenir une application angulaire propre. L'idée est de séparer les composants qui gèrent la logique, les données et la communication de ceux qui s'occupent uniquement de l'affichage des informations.

Composants intelligents

Les composants intelligents, également appelés composants conteneurs, sont des composants sensibles aux fonctionnalités. Ils peuvent utiliser des services, des magasins, des routeurs, des paramètres de routage et la logique d'application. Ils correspondent généralement à une page ou à un composant principal de la fonctionnalité.

Exemple de composants intelligents :

@Component({
  selector: 'app-projects-list-page',
  standalone: true,
  imports: [
    ProjectCardComponent,
    ProjectFiltersComponent,
    SpinnerComponent
  ],
  template: `
    <section>
      <h2>Progetti</h2>

      <app-project-filters
        [query]="store.query()"
        (queryChange)="store.setQuery($event)"
      />

      @if (store.loading()) {
        <app-spinner />
      } @else if (store.error()) {
        <p class="error">{{ store.error() }}</p>
      } @else {
        <div class="projects-grid">
          @for (project of store.filteredProjects(); track project.id) {
            <app-project-card
              [project]="project"
              (open)="openProject($event)"
            />
          }
        </div>
      }
    </section>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectsListPageComponent implements OnInit {
  readonly store = inject(ProjectsStore);
  private readonly router = inject(Router);

  ngOnInit(): void {
    this.store.loadProjects();
  }

  openProject(id: string): void {
    this.router.navigate(['/projects', id]);
  }
}

Ce composant est intelligent car il coordonne la page : il charge les données, lit la boutique, gère la navigation et transmet les informations aux composants enfants.

Composants stupides

Les composants stupides, également appelés composants de présentation, sont des composants simples, réutilisables et faciles à tester. Ils reçoivent des données via l'entrée et émettent des événements via la sortie. Ils ne devraient pas connaître les routeurs, les services HTTP ou les magasins mondiaux.

@Component({
  selector: 'app-project-filters',
  standalone: true,
  template: `
    <label>
      Cerca progetto
      <input
        type="search"
        [value]="query()"
        (input)="onInput($event)"
        placeholder="Cerca per titolo..."
      />
    </label>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectFiltersComponent {
  query = input('');
  queryChange = output<string>();

  onInput(event: Event): void {
    const target = event.target as HTMLInputElement;
    this.queryChange.emit(target.value);
  }
}

Ce composant ne sait rien de la fonctionnalité complète. Il ne sait pas d'où viennent les projets, il ne connaît pas l'API, il ne navigue pas, il ne modifie pas directement le store. Son travail consiste simplement à afficher une entrée et à communiquer la nouvelle valeur.

Pourquoi cette séparation fonctionne

Séparer les composants intelligents et stupides apporte des avantages concrets :

  • les composants de présentation sont plus faciles à réutiliser ;
  • les tests deviennent plus faciles ;
  • la logique applicative reste dans les conteneurs ou les magasins ;
  • l'interface utilisateur devient plus prévisible ;
  • réduit le risque de composants énormes et difficiles à entretenir.

Cependant, nous ne devons pas appliquer le modèle de manière dogmatique. Pour de très petits composants ou des fonctionnalités simples, un seul composant peut suffire. Le but n'est pas de créer le plus de dossiers possible, mais de maintenir des responsabilités claires.


Structure de dossiers recommandée pour une application angulaire évolutive

Une structure solide pour une application Angular moderne pourrait ressembler à ceci :

src/
  app/
    app.component.ts
    app.config.ts
    app.routes.ts

    core/
      auth/
        auth.service.ts
        auth.guard.ts
        auth.interceptor.ts
      http/
        api-error.interceptor.ts
      layout/
        main-layout.component.ts
        admin-layout.component.ts
      services/
        logger.service.ts
        storage.service.ts

    shared/
      ui/
        button/
          button.component.ts
        modal/
          modal.component.ts
        spinner/
          spinner.component.ts
      pipes/
        truncate.pipe.ts
      directives/
        autofocus.directive.ts
      models/
        pagination.model.ts
        api-response.model.ts
      utils/
        date.util.ts

    features/
      home/
        pages/
          home-page.component.ts

      projects/
        projects.routes.ts
        pages/
          projects-list-page.component.ts
          project-detail-page.component.ts
        components/
          project-card.component.ts
          project-filters.component.ts
        data-access/
          projects-api.service.ts
          projects-store.service.ts
        models/
          project.model.ts
        utils/
          project-status.util.ts

      blog/
        blog.routes.ts
        pages/
          blog-list-page.component.ts
          blog-detail-page.component.ts
        components/
          blog-card.component.ts
          blog-search.component.ts
        data-access/
          blog-api.service.ts
          blog-store.service.ts
        models/
          blog-post.model.ts

      admin/
        admin.routes.ts
        pages/
          admin-dashboard-page.component.ts
          edit-post-page.component.ts
        components/
          admin-sidebar.component.ts
          content-editor.component.ts
        data-access/
          admin-api.service.ts
          admin-store.service.ts
        models/
          admin-user.model.ts

  environments/
    environment.ts

Ce cadre est suffisamment simple pour être compris rapidement, mais suffisamment robuste pour évoluer au fil du temps.

Les dossiers principaux expliqués

app.config.ts contient des fournisseurs globaux : routeur, client HTTP, intercepteur, configurations globales et services d'application.

app.routes.ts contient uniquement les itinéraires principaux et délègue les itinéraires détaillés aux entités individuelles.

core contient ce qui vit globalement : authentification, intercepteur, mise en page, enregistreur, configurations et services d'infrastructure.

shared contient des éléments génériques et réutilisables, sans logique métier spécifique.

features contient le cœur de l'application, organisé par domaine fonctionnel.

pages contient des composants liés directement aux itinéraires. Ce sont généralement des composants intelligents.

components contient des composants spécifiques à des fonctionnalités, souvent stupides ou semi-présentatifs.

data-access contient des services API, un magasin, une façade et une logique d'accès aux données.

models contient des interfaces et des types TypeScript liés à la fonctionnalité.

utils contient des fonctions pures spécifiques à la fonctionnalité.


Couche d'accès aux données : isoler l'API et l'état

Dans une application Angular évolutive, les composants ne doivent pas communiquer directement avec HttpClient. Si chaque composant effectue des appels HTTP autonomes, la logique est dupliquée et il devient difficile de gérer le chargement, les erreurs, la mise en cache et la transformation des données.

Mieux vaut créer une couche accès aux données pour chaque entité. Ce niveau peut contenir :

  • Services API ;
  • magasins basés sur les signaux ;
  • façade;
  • mappeur entre les modèles DTO et UI ;
  • comprend une logique de mise en cache locale.

Exemple de service API :

@Injectable()
export class ProjectsApiService {
  private readonly http = inject(HttpClient);
  private readonly baseUrl = '/api/projects';

  getProjects(): Observable<Project[]> {
    return this.http.get<Project[]>(this.baseUrl);
  }

  getProjectById(id: string): Observable<Project> {
    return this.http.get<Project>(`${this.baseUrl}/${id}`);
  }
}

Le composant n'a pas besoin de connaître les points de terminaison, les URL ou les détails HTTP. Il doit communiquer avec le magasin ou avec une façade. Cela maintient l'interface utilisateur propre et facilite les tests.


Règles empiriques en matière de toxicomanie

Pour éviter le chaos architectural, il est utile d'établir des règles claires sur les dépendances :

  • Une fonctionnalité peut être importée depuis shared, car shared contient des éléments génériques.
  • Une fonctionnalité peut utiliser les services de base, tels que l'authentification ou l'enregistreur, si cela est vraiment nécessaire.
  • Le partage ne doit pas importer de fonctionnalités, sinon il cesse d'être générique.
  • Shared doit éviter les fortes dépendances sur le noyau, pour rester réutilisable.
  • Une fonctionnalité ne doit pas importer directement une autre fonctionnalité.
  • Le noyau ne doit pas contenir de logique spécifique à une seule fonctionnalité.

Une direction possible des dépendances est la suivante :

features  --->  shared
features  --->  core
core      --->  shared
shared    --->  nessuna feature

Plus ces règles sont respectées, plus l'application reste modulaire.


Conventions de dénomination recommandées

Les conventions de dénomination semblent être des détails, mais dans les grands projets, elles font une grande différence. Un nom cohérent réduit le temps nécessaire pour comprendre le rôle d'un fichier.

  • *.page.ts for components connected to a route.
  • *.component.ts pour les composants normaux de l'interface utilisateur.
  • *.service.ts for general services.
  • *.api.service.ts for services that communicate with backend.
  • *.store.ts ou *.store.service.ts pour l'état des fonctionnalités.
  • *.guard.ts pour le garde-route.
  • *.interceptor.ts pour l'intercepteur HTTP.
  • *.model.ts for main interfaces and types.
  • *.util.ts for pure support functions.

For example, project-detail-page.component.ts immediately communicates that it is a page. projects-api.service.ts communicates that that service talks to the backend. projects-store.service.ts communicates that that file manages the state of the feature.


Performance et architecture

A good Angular architecture is not just about file order. It also affects the performance of the application.

Using lazy-loaded features means avoiding loading all the code at startup. Si un utilisateur visite uniquement la page d'accueil, cela n'a aucun sens de télécharger immédiatement le code de l'administrateur, de l'éditeur, du tableau de bord et de toutes les sections internes.

Standalone components help because they make it easier to load components and routes in a granular way. Les signaux sont utiles car ils rendent le rendu plus précis et l'état plus facile à suivre. The smart/dumb pattern helps because it reduces huge components and difficult-to-optimize templates.

Quelques bonnes pratiques :

  • utiliser le chargement différé pour les fonctionnalités qui ne sont pas immédiatement nécessaires ;
  • utilisez ChangeDetectionStrategy.OnPush dans les composants ;
  • utilisez @for avec track pour des listes performantes ;
  • garder les composants petits et concentrés ;
  • évite une logique lourde directement dans le modèle ;
  • utiliser des signaux et calculer des valeurs dérivées ;
  • charger des images et des ressources de manière optimisée ;
  • séparez l'administrateur du frontend public autant que possible.

Common Mistakes to Avoid

1. Components too large

Un composant contenant d'énormes modèles, des appels HTTP, la gestion des formulaires, l'état, les validations, le mappage des données et la logique de navigation devient rapidement ingérable. Si un membre dépasse trop de responsabilités, il est temps de le diviser.

2. Global services for everything

Tous les services n'ont pas besoin d'être fournis dans : 'root'. Si un service ne dessert qu'une fonctionnalité, il peut être plus approprié de la fournir au niveau de l'itinéraire ou du composant. Cela réduit les états globaux inutiles et rend le cycle de vie plus prévisible.

3. Shared too large

Un dossier partagé volumineux est souvent le signe d'une architecture faible. Shared doit contenir des composants et des utilitaires véritablement génériques, et non des morceaux de fonctionnalités ajoutés pour plus de commodité.

4. Features paired together

Lorsqu'une fonctionnalité importe directement des fichiers d'une autre fonctionnalité, le projet devient fragile. Mieux vaut intégrer le code partagé dans un espace commun avec une responsabilité claire.

5. Business logic in template

Le modèle doit rester lisible. If a condition becomes too complex, move it to a computed, clear method, or feature store.

6. Manque de conventions

Si chaque développeur crée des dossiers et des noms à sa manière, le projet perd en cohérence. Les accords doivent être simples, documentés et respectés.


Practical example: a well-structured Blog feature

Let's imagine a blog section with article list, article detail, search, category filters and content management on the admin side. Une structure ordonnée pourrait être :

src/app/features/blog/
  blog.routes.ts

  pages/
    blog-list-page.component.ts
    blog-detail-page.component.ts

  components/
    blog-card.component.ts
    blog-search.component.ts
    blog-category-filter.component.ts
    blog-empty-state.component.ts

  data-access/
    blog-api.service.ts
    blog-store.service.ts

  models/
    blog-post.model.ts
    blog-category.model.ts

  utils/
    reading-time.util.ts
    slug.util.ts

The blog-list-page page is smart: use the store, load data, manage filters and search. Le composant blog-card est muet : il reçoit un article et affiche le titre, l'extrait, l'image et la date. Le service blog-api communique avec le backend. The store maintains status, loading, errors and filtered articles.

Ce type de séparation vous permet de modifier la disposition de la carte sans toucher à la logique des données, ou de modifier les points de terminaison de l'API sans modifier les composants de présentation.


Liste de contrôle finale pour une architecture angulaire évolutive

Before considering the structure of your Angular app solid, you can use this checklist:

  • Les principales fonctionnalités sont organisées en features?
  • Chaque fonctionnalité possède-t-elle des itinéraires, des pages, des composants, un accès aux données et des modèles distincts ?
  • Le code global est-il vraiment limité à core ?
  • shared contient uniquement des éléments génériques et réutilisables ?
  • Les fonctionnalités évitent-elles les dépendances directes entre elles ?
  • Les itinéraires principaux utilisent-ils le chargement différé ?
  • Les composants autonomes déclarent-ils clairement leurs dépendances ?
  • La logique HTTP est-elle extérieure aux composants ?
  • Le statut de l'élément est-il géré par des magasins, des façades ou des services dédiés ?
  • Des signaux sont-ils utilisés pour l'état local et les valeurs dérivées ?
  • RxJS est-il utilisé là où vous avez vraiment besoin de modéliser des flux asynchrones ?
  • Les composants intelligents et stupides ont-ils des responsabilités distinctes ?
  • Les noms de fichiers sont-ils cohérents ?
  • Les composants sont-ils petits, lisibles et testables ?
  • Y a-t-il une convention partagée par l'équipe ?

Conclusion

Bien organiser un projet Angular ne signifie pas créer une structure compliquée. Cela signifie créer une structure claire, prévisible et adaptée à la croissance. L'architecture basée sur les fonctionnalités vous aide à réfléchir aux fonctionnalités du monde réel. La séparation entre noyau et partage évite toute confusion. Les composants autonomes rendent les dépendances plus explicites. Les signaux simplifient la gestion de l'état. Le modèle de composants intelligents ou stupides améliore la lisibilité, la réutilisation et la testabilité.

Le plus important est de ne pas attendre que le projet devienne grand pour penser architecture. Les décisions prises au début influencent tout le cycle de vie de l’application. Une structure épurée vous permet d'ajouter de nouvelles pages, de nouvelles fonctionnalités et de nouveaux développeurs sans transformer le code en labyrinthe.

Si vous créez une application Angular professionnelle, partez d'une règle simple : tout doit avoir une place spécifique. Les fonctionnalités doivent contenir la logique du produit, le noyau doit contenir l'infrastructure globale, et le partage doit contenir des éléments véritablement réutilisables. À partir de là, ajoutez des composants autonomes, un chargement paresseux, des signaux et une séparation claire entre les composants intelligents et stupides.

Une architecture évolutive n'est pas la plus complexe, mais celle que l'équipe peut comprendre, maintenir et faire évoluer au fil du temps. Angular propose tous les outils nécessaires : à nous de les utiliser avec discipline, cohérence et bon sens.

💬 Notes des lecteurs

0 notes

Écrire une note

Partagez votre avis, une suggestion ou un compliment

Dernières notes

Aucune note pour le moment. Soyez le premier à commenter !