Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
NestJS

L'architecture d'une application NestJS : vue d'ensemble

Cette leçon donne une vue d'ensemble de l'architecture d'une application NestJS : les trois briques fondamentales (modules, contrôleurs, providers), les enrichisseurs qui s'insèrent sur le trajet des ...

0 chapitres 0 QCM Accès gratuit
L'architecture d'une application NestJS : vue d'ensemble

À propos de ce cours

Cette leçon donne une vue d'ensemble de l'architecture d'une application NestJS : les trois briques fondamentales (modules, contrôleurs, providers), les enrichisseurs qui s'insèrent sur le trajet des requêtes (middleware, guards, intercepteurs, pipes, filtres d'exception), l'arbre des modules, le conteneur d'injection de dépendances, l'architecture en couches contrôleur, service et accès aux données, l'organisation d'un projet par fonctionnalités et la comparaison avec Angular. Elle s'appuie sur les journaux de démarrage réels d'une application et propose six activités corrigées, dont la refactorisation d'un contrôleur trop chargé.

Objectifs de la leçon

  • Identifier les trois briques d'une application NestJS et leur rôle.
  • Situer les enrichisseurs (middleware, guards, intercepteurs, pipes, filtres).
  • Lire l'arbre des modules d'une application.
  • Répartir le code entre contrôleur, service et accès aux données.
  • Organiser un projet par fonctionnalités.
Une architecture imposée… et c'est une bonne nouvelle

Au chapitre 01, nous avons vu qu'Express laisse chaque développeur organiser son code comme il l'entend. NestJS fait le choix inverse : il impose une architecture. Tous les projets NestJS se ressemblent, ce qui permet à un nouveau développeur (ou à un enseignant qui corrige un TP !) de s'y retrouver en quelques minutes.

Cette architecture repose sur trois briques et sur des enrichisseurs qui s'intercalent sur le trajet des requêtes.

1. Les trois briques fondamentales
BriqueDécorateurRôleAnalogie
Module@Module()Regroupe tout ce qui concerne une fonctionnalité et déclare ce qu'il partageun service d'un établissement (scolarité, comptabilité)
Contrôleur@Controller()Reçoit les requêtes HTTP d'une ressource et renvoie les réponsesle guichet d'accueil
Provider (souvent un service)@Injectable()Contient la logique métier et l'accès aux données ; injecté là où on en a besoinles agents qui traitent les dossiers
@Module({
  imports: [GroupesModule],               // modules dont on utilise les services
  controllers: [StagiairesController],    // guichets HTTP de ce module
  providers: [StagiairesService],         // services de ce module
  exports: [StagiairesService],           // ce que l'on partage avec les autres modules
})
export class StagiairesModule {}
2. L'arbre des modules

Toute application a un module racine, AppModule, passé à NestFactory.create() dans main.ts. Il importe des modules de fonctionnalité, qui peuvent à leur tour en importer d'autres. NestJS parcourt cet arbre au démarrage pour tout construire.

Arbre des modules d'une application NestJS

AppModule importe les modules de fonctionnalité ; ceux-ci importent les modules dont ils utilisent les services.

Les journaux de démarrage montrent ce travail. Voici ceux, réels, de l'application de la leçon 5.2 (deux modules : groupes et stagiaires) :

Terminal au démarrage :

[Nest] LOG [NestFactory] Starting Nest application...
[Nest] LOG [InstanceLoader] AppModule dependencies initialized +13ms
[Nest] LOG [InstanceLoader] GroupesModule dependencies initialized +0ms
[Nest] LOG [InstanceLoader] StagiairesModule dependencies initialized +0ms
[Nest] LOG [RoutesResolver] GroupesController {/api/groupes}: +4ms
[Nest] LOG [RouterExplorer] Mapped {/api/groupes, GET} route +4ms
[Nest] LOG [RouterExplorer] Mapped {/api/groupes/:code, GET} route +1ms
[Nest] LOG [RoutesResolver] StagiairesController {/api/stagiaires}: +0ms
[Nest] LOG [RouterExplorer] Mapped {/api/stagiaires/:id, GET} route +1ms
[Nest] LOG [NestApplication] Nest application successfully started +2ms
  • InstanceLoader: chaque module est chargé et ses providers créés (injection de dépendances résolue).
  • RoutesResolver / RouterExplorer: chaque contrôleur est inspecté et ses routes enregistrées, grâce aux métadonnées des décorateurs (leçon 4.7).
3. Le conteneur d'injection de dépendances

Vous n'écrivez jamais new StagiairesService(). Le conteneur (ou injecteur) de NestJS :

  1. lit les providers de chaque module ;
  2. lit le type des paramètres de chaque constructeur (design:paramtypes, leçon 4.7) ;
  3. crée les instances dans le bon ordre (d'abord les dépendances) et les relie ;
  4. par défaut, crée une seule instance de chaque provider (singleton), partagée par tous ceux qui l'injectent.

C'est exactement le mini-injecteur que vous avez construit au chapitre 04… en version industrielle, qui gère aussi les modules, la visibilité (exports), les portées et les dépendances circulaires (chapitre 07).

4. Les enrichisseurs : sur le trajet des requêtes

Entre l'arrivée d'une requête et l'exécution du contrôleur, NestJS peut faire intervenir des classes spécialisées. Chacune a un rôle précis :

EnrichisseurQuestion à laquelle il répondExempleChapitre
MiddlewareQue faire de toute requête, avant le routage ?journaliser, ajouter un identifiant de requête09
GuardCette requête a-t-elle le droit de passer ?utilisateur connecté, rôle admin09, 13
IntercepteurQue faire avant et après le contrôleur ?mesurer le temps, formater la réponse, cache09
PipeLes données reçues sont-elles valides ? Faut-il les convertir ?« 12 » → 12, valider un DTO08
Filtre d'exceptionComment répondre quand une erreur survient ?format d'erreur JSON unifié09

Leur ordre d'exécution est détaillé à la leçon 5.5.

5. L'architecture en couches

À l'intérieur d'un module, le code se répartit en couches, chacune avec une seule responsabilité :

CoucheFaitNe fait pas
Contrôleurlire la requête (paramètres, corps), appeler le service, renvoyer le résultatcalculs, règles métier, SQL
Servicerègles métier, calculs, orchestration de plusieurs sourceslire req/res, connaître HTTP
Accès aux données (repository)lire et écrire dans la baserègles métier

Exemple à ne pas suivre: un contrôleur « obèse » qui fait tout.

@Controller('stagiaires')
export class StagiairesController {
  private stagiaires = [{ id: 1, nom: 'Ben Ali', notes: [14, 9, 16] }];

  @Get(':id/bulletin')
  bulletin(@Param('id') id: string, @Res() res: Response) {
    const s = this.stagiaires.find((x) => x.id === Number(id));
    if (!s) return res.status(404).json({ message: 'Introuvable' });
    const moyenne = s.notes.reduce((a, b) => a + b, 0) / s.notes.length;   // règle métier
    const mention = moyenne >= 16 ? 'Très bien' : moyenne >= 14 ? 'Bien' : 'Passable';
    return res.json({ nom: s.nom, moyenne, mention });
  }
}

Problèmes : la règle de calcul est enfermée dans une route HTTP (impossible à réutiliser dans un export PDF ou une tâche planifiée), le contrôleur manipule directement la réponse Express, et le test unitaire du calcul exige de simuler une requête HTTP.

Version en couches:

@Injectable()
export class BulletinsService {
  constructor(private readonly stagiaires: StagiairesService) {}

  calculer(id: number) {
    const s = this.stagiaires.findOne(id);              // lève NotFoundException si absent
    const moyenne = s.notes.reduce((a, b) => a + b, 0) / s.notes.length;
    return { nom: s.nom, moyenne: +moyenne.toFixed(2), mention: this.mention(moyenne) };
  }

  mention(moyenne: number): string {
    return moyenne >= 16 ? 'Très bien' : moyenne >= 14 ? 'Bien' : moyenne >= 10 ? 'Passable' : 'Ajourné';
  }
}

@Controller('stagiaires')
export class StagiairesController {
  constructor(private readonly bulletins: BulletinsService) {}

  @Get(':id/bulletin')
  bulletin(@Param('id') id: string) {
    return this.bulletins.calculer(Number(id));          // le contrôleur délègue
  }
}
6. Organiser le projet par fonctionnalités
src/
├── main.ts
├── app.module.ts
├── common/                 ← éléments partagés : services utilitaires, middleware, décorateurs
│   ├── common.module.ts
│   └── journal.service.ts
├── config/                 ← configuration (chapitre 10)
├── formations/             ← une fonctionnalité = un dossier = un module
│   ├── dto/
│   ├── formations.controller.ts
│   ├── formations.module.ts
│   └── formations.service.ts
├── groupes/
└── stagiaires/

Quand une fonctionnalité grandit, on peut la découper en sous-modules (par exemple stagiaires/absences). La règle : tout ce qui change ensemble reste ensemble.

7. Si vous connaissez Angular
AngularNestJSRemarque
@NgModule / composants standalone@Moduleles modules restent la norme dans NestJS
@Component@Controllerun contrôleur n'a pas de template : il renvoie des données
@Injectable() service@Injectable() providermême décorateur, même principe
providedIn: 'root'providers + exports ou @Global()dans NestJS, la visibilité se déclare dans les modules
Guards du routeur, HttpInterceptor, pipes de templateGuards, intercepteurs, pipesmêmes noms, côté serveur
ngOnInit, ngOnDestroyonModuleInit, onModuleDestroyleçon 5.6
Activités

Activité 1 Quelle couche ?

Pour chaque morceau de code, indiquez s'il doit se trouver dans le contrôleur, le service ou la couche d'accès aux données :

  1. Lire le paramètre :id de l'URL.
  2. Vérifier qu'un groupe n'est pas complet avant d'y inscrire un stagiaire.
  3. Exécuter SELECT * FROM stagiaire WHERE groupe = ?.
  4. Choisir le code de statut 201 pour une création.
  5. Calculer le taux de réussite d'un groupe.
  6. Envoyer un e-mail de confirmation après une inscription (via un MailService).
Voir le corrigé

1 : contrôleur ; 2 : service (règle métier) ; 3 : accès aux données ; 4 : contrôleur (détail HTTP, avec @HttpCode ou par défaut) ; 5 : service ; 6 : service (le service d'inscription appelle MailService, injecté).

Activité 2 Refactoriser un contrôleur obèse

Dans le projet api-formation, créez un module notes (nest g mo notes, nest g co notes, nest g s notes). Écrivez d'abord une route GET /api/notes/groupe/:code/moyenne entièrement dans le contrôleur (tableau de notes en dur, calcul dans la méthode), puis déplacez données et calcul dans NotesService pour que la méthode du contrôleur tienne en une ligne.

Voir le corrigé
// notes.service.ts
@Injectable()
export class NotesService {
  private notes = [
    { stagiaire: 'Ben Ali', groupe: 'DEV101', valeur: 14 },
    { stagiaire: 'Trabelsi', groupe: 'DEV101', valeur: 17 },
    { stagiaire: 'Gharbi', groupe: 'DEV102', valeur: 9 },
  ];

  moyenneGroupe(code: string) {
    const valeurs = this.notes.filter((n) => n.groupe === code.toUpperCase()).map((n) => n.valeur);
    if (valeurs.length === 0) throw new NotFoundException(`Aucune note pour le groupe ${code}`);
    return { groupe: code.toUpperCase(), moyenne: valeurs.reduce((a, b) => a + b, 0) / valeurs.length };
  }
}

// notes.controller.ts
@Controller('notes')
export class NotesController {
  constructor(private readonly notesService: NotesService) {}

  @Get('groupe/:code/moyenne')
  moyenne(@Param('code') code: string) {
    return this.notesService.moyenneGroupe(code);
  }
}

GET /api/notes/groupe/dev101/moyenne renvoie { "groupe": "DEV101", "moyenne": 15.5 }.

Activité 3 Dessiner l'arbre des modules

Une application de gestion des absences comporte : les stagiaires, les groupes, les séances de cours, les absences (une absence concerne un stagiaire et une séance), les notifications par e-mail (utilisées par les absences) et un service de journalisation utilisé partout. Proposez les modules, ce que chacun exporte, et dessinez l'arbre des imports.

Voir le corrigé
  • CommonModule: exporte JournalService; importé par les modules qui journalisent (ou rendu global).
  • GroupesModule: exporte GroupesService.
  • StagiairesModule: importe GroupesModule; exporte StagiairesService.
  • SeancesModule: importe GroupesModule; exporte SeancesService.
  • NotificationsModule: exporte MailService.
  • AbsencesModule: importe StagiairesModule, SeancesModule et NotificationsModule; n'exporte rien (personne ne dépend de lui).
  • AppModule: importe tous les modules de fonctionnalité.

Les dépendances vont toujours dans le même sens (absences → stagiaires → groupes) : pas de cycle.

Activité 4 Générer et observer

Dans api-formation, générez les modules formations et groupes avec contrôleur et service (nest g mo, nest g co, nest g s, avec --no-spec). Lancez l'application et relevez dans le terminal : (a) les lignes InstanceLoader; (b) les lignes RoutesResolver. Ouvrez app.module.ts: qu'a ajouté la CLI ?

Voir le corrigé

Le terminal affiche une ligne InstanceLoader … dependencies initialized par module (AppModule, FormationsModule, GroupesModule) et une ligne RoutesResolver par contrôleur (FormationsController {/api/formations}…). Les contrôleurs générés n'ont pas encore de route : aucune ligne Mapped pour eux. La CLI a ajouté FormationsModule et GroupesModule dans les imports d'AppModule.

Activité 5 Associer les enrichisseurs

Quel enrichisseur utiliser pour chaque besoin ? (a) Refuser l'accès à DELETE /stagiaires/:id aux non-administrateurs ; (b) convertir :id en nombre et renvoyer 400 si ce n'est pas un nombre ; (c) ajouter à chaque réponse un champ dureeMs; (d) journaliser toutes les requêtes, même celles vers des routes inexistantes ; (e) renvoyer toutes les erreurs au format { code, message, date }.

Voir le corrigé

(a) guard ; (b) pipe (ParseIntPipe) ; (c) intercepteur ; (d) middleware (il s'exécute avant le routage, donc même pour une route inconnue) ; (e) filtre d'exception.

Activité 6 Comparer avec Angular

Dans votre projet Angular du cours précédent, retrouvez un service injecté dans un composant. Écrivez son équivalent côté NestJS : un service injecté dans un contrôleur, dans un module. Quelle différence principale constatez-vous dans la façon de rendre le service disponible ?

Voir le corrigé

En Angular, @Injectable({ providedIn: 'root' }) rend le service disponible dans toute l'application sans le déclarer ailleurs. En NestJS, un service doit être déclaré dans les providers d'un module, et il n'est visible hors de ce module que s'il est exporté et que le module est importé (sauf module global). Cette encapsulation stricte est l'objet de la leçon suivante.

À retenir

  • Trois briques : modules (@Module), contrôleurs (@Controller), providers (@Injectable).
  • AppModule est la racine d'un arbre de modules parcouru au démarrage.
  • Le conteneur crée et relie les instances (singletons par défaut) : jamais de new pour un service.
  • Middleware, guards, intercepteurs, pipes et filtres enrichissent le trajet des requêtes.
  • Contrôleur mince, logique dans le service, données dans un repository.
  • Un dossier et un module par fonctionnalité, common/ pour le partagé.