À propos de ce cours
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
| Brique | Décorateur | Rôle | Analogie |
|---|---|---|---|
| Module | @Module() | Regroupe tout ce qui concerne une fonctionnalité et déclare ce qu'il partage | un service d'un établissement (scolarité, comptabilité) |
| Contrôleur | @Controller() | Reçoit les requêtes HTTP d'une ressource et renvoie les réponses | le 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 besoin | les 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.
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 :
- lit les
providersde chaque module ; - lit le type des paramètres de chaque constructeur (
design:paramtypes, leçon 4.7) ; - crée les instances dans le bon ordre (d'abord les dépendances) et les relie ;
- 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 :
| Enrichisseur | Question à laquelle il répond | Exemple | Chapitre |
|---|---|---|---|
| Middleware | Que faire de toute requête, avant le routage ? | journaliser, ajouter un identifiant de requête | 09 |
| Guard | Cette requête a-t-elle le droit de passer ? | utilisateur connecté, rôle admin | 09, 13 |
| Intercepteur | Que faire avant et après le contrôleur ? | mesurer le temps, formater la réponse, cache | 09 |
| Pipe | Les données reçues sont-elles valides ? Faut-il les convertir ? | « 12 » → 12, valider un DTO | 08 |
| Filtre d'exception | Comment 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é :
| Couche | Fait | Ne fait pas |
|---|---|---|
| Contrôleur | lire la requête (paramètres, corps), appeler le service, renvoyer le résultat | calculs, règles métier, SQL |
| Service | règles métier, calculs, orchestration de plusieurs sources | lire req/res, connaître HTTP |
| Accès aux données (repository) | lire et écrire dans la base | rè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
| Angular | NestJS | Remarque |
|---|---|---|
@NgModule / composants standalone | @Module | les modules restent la norme dans NestJS |
@Component | @Controller | un contrôleur n'a pas de template : il renvoie des données |
@Injectable() service | @Injectable() provider | mê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 template | Guards, intercepteurs, pipes | mêmes noms, côté serveur |
ngOnInit, ngOnDestroy | onModuleInit, onModuleDestroy | leç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 :
- Lire le paramètre
:idde l'URL. - Vérifier qu'un groupe n'est pas complet avant d'y inscrire un stagiaire.
- Exécuter
SELECT * FROM stagiaire WHERE groupe = ?. - Choisir le code de statut 201 pour une création.
- Calculer le taux de réussite d'un groupe.
- 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: exporteJournalService; importé par les modules qui journalisent (ou rendu global).GroupesModule: exporteGroupesService.StagiairesModule: importeGroupesModule; exporteStagiairesService.SeancesModule: importeGroupesModule; exporteSeancesService.NotificationsModule: exporteMailService.AbsencesModule: importeStagiairesModule,SeancesModuleetNotificationsModule; 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é.