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

Les contrôleurs NestJS

Cette leçon présente les contrôleurs, la porte d'entrée HTTP d'une application NestJS : leur rôle, leur génération avec la CLI, l'anatomie d'une classe contrôleur (préfixe, service injecté, décorateur...

0 chapitres 0 QCM Accès gratuit
Les contrôleurs NestJS

À propos de ce cours

Cette leçon présente les contrôleurs, la porte d'entrée HTTP d'une application NestJS : leur rôle, leur génération avec la CLI, l'anatomie d'une classe contrôleur (préfixe, service injecté, décorateurs de méthode et de paramètre), leur enregistrement dans un module et la façon dont NestJS transforme la valeur renvoyée en réponse HTTP (texte, JSON, promesse, Observable, réponse vide), démontrée par des requêtes réelles. Elle aborde aussi l'accès à l'objet réponse d'Express avec passthrough, les conventions de nommage et les bonnes pratiques d'un contrôleur mince, avec cinq activités corrigées.

Objectifs de la leçon

  • Expliquer le rôle d'un contrôleur dans une application NestJS.
  • Créer un contrôleur avec la CLI et l'enregistrer dans un module.
  • Comprendre comment NestJS transforme la valeur renvoyée en réponse HTTP.
  • Écrire des contrôleurs minces qui délèguent au service.
Le rôle d'un contrôleur

Un contrôleur est le guichet HTTP d'une ressource. Il reçoit les requêtes qui la concernent (/formations, /stagiaires…), en extrait les données utiles (paramètres, corps, en-têtes), appelle le service approprié et renvoie le résultat. NestJS se charge du reste : choisir la bonne méthode selon l'URL et le verbe HTTP, convertir le résultat en JSON, fixer le code de statut, transformer les exceptions en réponses d'erreur.

Anatomie d'un contrôleur NestJS

Préfixe, service injecté, décorateur de route, paramètre et valeur renvoyée.

Créer un contrôleur
nest g co formations            # crée src/formations/formations.controller.ts (+ .spec.ts)
nest g co formations --no-spec  # sans fichier de test
nest g co formations --flat     # directement dans src/, sans sous-dossier

La CLI crée la classe et l'ajoute automatiquement au tableau controllers du module le plus proche (formations.module.ts s'il existe, sinon app.module.ts). Un contrôleur qui n'est déclaré dans aucun module n'existe pas pour NestJS : ses routes renvoient 404.

// formations.controller.ts (généré)
import { Controller } from '@nestjs/common';

@Controller('formations')
export class FormationsController {}

// formations.module.ts
@Module({
  controllers: [FormationsController],   // ← indispensable
  providers: [FormationsService],
})
export class FormationsModule {}
Le décorateur @Controller
ÉcritureEffet
@Controller()pas de préfixe : les routes partent de la racine (ou du préfixe global)
@Controller('formations')toutes les routes commencent par /formations
@Controller(['formations', 'cursus'])deux préfixes équivalents
@Controller({ path: 'formations', version: '2' })préfixe et version de l'API (leçon 6.6)

Par convention, le préfixe est le nom de la ressource au pluriel, en minuscules et en kebab-case (groupes-formation), et la classe porte le même nom suivi de Controller.

Ce que NestJS fait de la valeur renvoyée

Contrairement à Express, on n'écrit pas res.json(…): on renvoie une valeur, et NestJS construit la réponse. Démonstration avec un contrôleur qui renvoie toutes sortes de valeurs :

📄 src/demo.controller.ts

import { Controller, Get } from '@nestjs/common';
import { Observable, of } from 'rxjs';

@Controller('demo')
export class DemoController {
  @Get('texte')
  texte(): string {
    return 'Bonjour depuis NestJS';                 // chaîne → envoyée telle quelle
  }

  @Get('objet')
  objet() {
    return { formation: 'NestJS', heures: 120 };    // objet → JSON
  }

  @Get('tableau')
  tableau() {
    return ['DEV101', 'DEV102'];                    // tableau → JSON
  }

  @Get('nombre')
  nombre(): number {
    return 42;                                      // nombre → JSON
  }

  @Get('promesse')
  async promesse() {
    await new Promise((r) => setTimeout(r, 50));    // NestJS attend la promesse
    return { charge: true };
  }

  @Get('observable')
  observable(): Observable<string[]> {
    return of(['a', 'b']);                          // NestJS s'abonne à l'Observable
  }

  @Get('rien')
  rien(): void {}                                   // pas de valeur → réponse vide
}

Requêtes envoyées et réponses réellement obtenues :

GET /api/demo/texte → 200 Bonjour depuis NestJS  [content-type: text/html; charset=utf-8]
GET /api/demo/objet → 200 {"formation":"NestJS","heures":120}  [content-type: application/json; charset=utf-8]
GET /api/demo/tableau → 200 ["DEV101","DEV102"]  [content-type: application/json; charset=utf-8]
GET /api/demo/nombre → 200 42  [content-type: text/html; charset=utf-8]
GET /api/demo/promesse → 200 {"charge":true}  [content-type: application/json; charset=utf-8]
GET /api/demo/observable → 200 ["a","b"]  [content-type: application/json; charset=utf-8]
GET /api/demo/rien → 200  [content-type: null]
  • Objets et tableaux sont convertis en JSON, avec l'en-tête Content-Type: application/json.
  • Chaînes et nombres sont envoyés tels quels : le type de contenu est alors text/html. Pour une API, préférez renvoyer un objet ({ message: '…' }) : le client saura toujours qu'il reçoit du JSON.
  • Une promesse (méthode async) est attendue ; un Observable RxJS est écouté jusqu'à sa dernière valeur.
  • Une méthode qui ne renvoie rien produit une réponse vide avec le statut 200 (204 serait plus juste : leçon 6.5).
  • Le statut est 200 par défaut, 201 pour @Post().
Et si j'ai besoin de l'objet réponse d'Express ?

On peut injecter l'objet réponse d'Express avec @Res(). Mais NestJS considère alors que vous gérez la réponse : la valeur renvoyée est ignorée, et les intercepteurs ne s'appliquent plus. Pour simplement ajouter un en-tête tout en laissant NestJS envoyer la réponse, utilisez l'option passthrough:

import type { Response } from 'express';

@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.setHeader('X-Total-Count', '42');   // on complète la réponse…
  return this.formationsService.findAll(); // …et NestJS l'envoie normalement
}

import type { Response } from 'express': le mot type indique qu'on importe uniquement un type, effacé à la compilation. C'est nécessaire avec les options de compilation récentes (isolatedModules) quand un type est utilisé dans les paramètres d'une méthode décorée.

Un contrôleur mince

Au chapitre 05, nous avons vu qu'un contrôleur ne contient ni calcul ni règle métier. Chaque méthode suit le même schéma en trois temps :

  1. lire les données de la requête (décorateurs de paramètre) ;
  2. appeler une méthode du service ;
  3. renvoyer son résultat.
@Controller('formations')
export class FormationsController {
  constructor(private readonly formationsService: FormationsService) {}

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {     // 1. lire
    return this.formationsService.findOne(id);          // 2. appeler + 3. renvoyer
  }
}

Un module peut contenir plusieurs contrôleurs (par exemple FormationsController et FormationsAdminController), qui partagent les mêmes services.

Activités

Activité 1 Générer et enregistrer

Dans api-formation, générez un contrôleur salles sans module (nest g co salles --no-spec). Dans quel module la CLI l'a-t-elle déclaré ? Ajoutez une méthode @Get() qui renvoie [{ code: 'S1', places: 24 }] et vérifiez GET /api/salles. Supprimez ensuite la déclaration du module : que renvoie la même requête ?

Voir le corrigé

Sans SallesModule, la CLI déclare le contrôleur dans AppModule. La route renvoie le tableau en JSON. Sans déclaration, le terminal n'affiche plus RoutesResolver SallesController et la requête renvoie 404 « Cannot GET /api/salles ».

Activité 2 Texte ou JSON ?

Ajoutez deux routes : GET /api/salles/message qui renvoie la chaîne « Salles disponibles » et GET /api/salles/message-json qui renvoie { message: 'Salles disponibles' }. Comparez l'en-tête Content-Type des deux réponses (Thunder Client, onglet Headers). Laquelle préférer pour une API, et pourquoi ?

Voir le corrigé

La première renvoie text/html; charset=utf-8, la seconde application/json; charset=utf-8. Pour une API, on renvoie toujours du JSON : le client (Angular, Flutter) peut traiter toutes les réponses de la même façon, et ajouter plus tard d'autres champs ne casse rien.

Activité 3 Promesse et délai

Écrivez une route GET /api/salles/disponibilites, méthode async, qui attend 300 ms (simulation d'une requête en base) puis renvoie { salles: 3, libres: 1 }. Mesurez le temps de réponse dans votre client d'API. Que se passe-t-il pour une autre requête envoyée pendant ces 300 ms ?

Voir le corrigé

La réponse arrive après environ 300 ms. Une autre requête envoyée pendant l'attente est traitée immédiatement : l'attente est asynchrone et ne bloque pas Node.js (leçon 4.3). Si l'on remplaçait l'attente par une boucle de calcul de 300 ms, toutes les requêtes seraient bloquées.

Activité 4 Ajouter un en-tête sans perdre NestJS

Sur GET /api/salles, ajoutez l'en-tête X-Nombre-Salles. Faites-le une fois avec @Res() sans passthrough en gardant le return: que reçoit le client ? Corrigez.

Voir le corrigé

Avec @Res() seul, NestJS n'envoie plus la valeur renvoyée : la requête reste en attente jusqu'au délai maximum du client, car personne n'appelle res.json(). Correction : @Res({ passthrough: true }) res: Response, puis res.setHeader('X-Nombre-Salles', '1') et return [...].

Activité 5 Repérer le contrôleur obèse

Réécrivez cette méthode pour qu'elle respecte le schéma « lire, appeler, renvoyer » :

@Get('taux-occupation')
taux() {
  const salles = [{ code: 'S1', places: 24, occupees: 20 }, { code: 'S2', places: 16, occupees: 4 }];
  const total = salles.reduce((t, s) => t + s.places, 0);
  const occupees = salles.reduce((t, s) => t + s.occupees, 0);
  return { taux: Math.round((occupees / total) * 100) + '%' };
}
Voir le corrigé
// salles.service.ts
@Injectable()
export class SallesService {
  private salles = [{ code: 'S1', places: 24, occupees: 20 }, { code: 'S2', places: 16, occupees: 4 }];

  tauxOccupation() {
    const total = this.salles.reduce((t, s) => t + s.places, 0);
    const occupees = this.salles.reduce((t, s) => t + s.occupees, 0);
    return { taux: Math.round((occupees / total) * 100) };   // un nombre : le client le formate
  }
}

// salles.controller.ts
@Get('taux-occupation')
taux() {
  return this.sallesService.tauxOccupation();
}

Le calcul devient réutilisable (tableau de bord, export) et testable sans HTTP. On renvoie aussi un nombre plutôt que « 60% » : la mise en forme est l'affaire du front-end.

À retenir

  • Un contrôleur reçoit les requêtes d'une ressource et délègue au service.
  • @Controller('ressources') fixe le préfixe ; le contrôleur doit être déclaré dans un module.
  • On renvoie une valeur : objet/tableau → JSON, chaîne/nombre → texte, promesse/Observable → attendus.
  • Statut 200 par défaut, 201 pour POST.
  • @Res() fait perdre la gestion automatique : préférer @Res({ passthrough: true }).