Cours NestJS · Module 1: Bases de NestJS

Les contrôleurs - les centurions de l'Empire

6 min de lecture
Dans cette leçon5

Un messager arrive à la porte du camp avec une requête : GET /legion/5. Le serveur l'a reçue - mais qui doit la traiter ? L'application compte des dizaines de méthodes et aucune ne sait que celle-ci lui est destinée.

Dans une légion, la répartition des ordres revient au centurion. Il se tient entre le messager et les soldats : il prend l'ordre, reconnaît qui il concerne, le transmet et renvoie la réponse. Il n'exécute pas le travail lui-même - il le dirige. Dans NestJS, ce centurion est un contrôleur.

Déclarer un contrôleur

Un contrôleur est une classe munie d'un décorateur indiquant quelle portion de routes il traite :

1@Controller('legion')
2export class LegionController { }

L'ordre est toujours le même : @Controller('legion'), puis export class, et enfin le nom de la classe avec son corps.

L'argument 'legion' est un préfixe de route. Toutes les méthodes de cette classe serviront les adresses commençant par /legion - et tu n'as pas à le répéter sur chacune. Un contrôleur sans argument, @Controller(), prend les routes depuis la racine.

Quatre verbes

À l'intérieur du contrôleur, chaque méthode reçoit un décorateur disant à quelle requête elle répond :

1@Controller('tributes')
2export class TributesController {
3  @Get()
4  findAll() { }
5
6  @Post()
7  create() { }
8
9  @Put(':id')
10  update() { }
11
12  @Delete(':id')
13  remove() { }
14}

Quatre décorateurs correspondent à quatre actions sur une ressource. @Get() récupère, @Post() crée une nouvelle ressource, @Put() met à jour une ressource existante, @Delete() la supprime.

Distinguer @Post de @Put prête à confusion, alors retiens-les par l'effet de la répétition : envoyer deux fois le même @Post crée deux ressources, tandis qu'envoyer deux fois le même @Put en laisse une, simplement écrite deux fois.

L'argument du décorateur ajoute un morceau de route. @Put(':id') dans un contrôleur préfixé tributes servira /tributes/42. Le deux-points marque un paramètre - un emplacement où tombe n'importe quelle valeur.

Trois endroits d'où viennent les données

Puisqu'une route peut contenir un paramètre, il faut bien le lire. Les données arrivent dans une requête par trois chemins, et chacun a son décorateur :

1@Get('search')
2search(@Query('province') province: string) { }
3
4@Get(':id')
5findOne(@Param('id') id: string) { }
6
7@Post()
8create(@Body() dto: CreateTributeDto) { }

@Param('id') extrait une valeur du chemin de l'adresse - c'est lui qui lit 5 dans /legion/5. @Query('province') prend un paramètre de requête, ce qui se trouve après le point d'interrogation : ?province=rome. @Body() va chercher le corps de la requête, envoyé avec @Post et @Put.

Remarque l'ordre des deux premières méthodes. NestJS associe les routes dans l'ordre de leur déclaration, et :id correspond à n'importe quel texte - y compris le mot search. Si findOne venait en premier, une requête GET /legion/search y arriverait avec id égal à 'search'. C'est pourquoi on déclare les routes à segment fixe avant les routes avec paramètre.

Ces trois-là couvrent tout ce dont tu as habituellement besoin. Attention à deux noms qui sonnent vraisemblables : @Path() n'existe pas dans NestJS - le chemin, c'est @Param. @Header() existe mais fait autre chose : il pose un en-tête de réponse. Pour lire les en-têtes de la requête, il y a @Headers(), au pluriel.

Une méthode de contrôleur vue de l'intérieur

Assemblons le tout. L'ordre des éléments d'une méthode ne varie jamais :

1@Get()
2getAllLegionaries() {
3  return this.service.findAll();
4}

D'abord le décorateur @Get(), puis le nom de la méthode, ensuite le corps entre accolades, et à l'intérieur return this.service.findAll().

Et l'on voit ici ce qu'est vraiment un contrôleur. Cette méthode ne contient aucune logique - elle prend la requête et la confie à un service. Un centurion ne forge pas les épées ; il sait chez quel forgeron envoyer.

C'est une règle que je te recommande de tenir dès le premier jour : pas de requêtes en base, de calculs ni de règles métier dans un contrôleur. Quand une méthode de contrôleur dépasse quelques lignes, c'est le signe qu'elle fait quelque chose qui appartient à un service. Le gain est pratique - tu appelleras ensuite cette même logique depuis une tâche par lots ou un consommateur de file, où il n'y a aucun HTTP.

Le contrôleur n'est pas non plus le premier arrêt de la requête. En chemin se trouvent des couches que tu découvriras dans les prochains modules : d'abord les middlewares, puis les guards (des sentinelles qui décident de laisser passer ou non la requête), ensuite les intercepteurs (avant l'appel de la méthode) et les pipes (ils vérifient et convertissent les données, par exemple ValidationPipe). Ce n'est qu'ensuite que la méthode du contrôleur s'exécute, et son résultat revient au client en passant par les intercepteurs.

Note que nous ne construisons pas la réponse à la main. La valeur retournée sera transformée en JSON, et NestJS choisit lui-même le code de statut : 200 pour la plupart des méthodes, 201 pour @Post, puisque quelque chose a été créé.

Récapitulatif

Le centurion se tient à la porte et sait à qui transmettre l'ordre :

  • un contrôleur reçoit une requête et l'oriente plus loin - il n'exécute pas le travail lui-même,
  • déclaration dans l'ordre : @Controller('prefixe'), export class, le nom de la classe,
  • l'argument de @Controller est un préfixe de route commun à toutes les méthodes de la classe,
  • quatre verbes : @Get() récupère, @Post() crée une nouvelle ressource, @Put() met à jour, @Delete() supprime,
  • un @Post répété crée deux ressources, un @Put répété en laisse une,
  • le deux-points dans une route marque un paramètre : @Put(':id') sert /tributes/42,
  • déclare une route fixe (search) avant une route avec paramètre (:id), sinon le paramètre capterait son adresse,
  • @Param('id') lit dans le chemin, @Query('nom') lit un paramètre de requête après le ?, @Body() lit le corps de la requête,
  • @Path() n'existe pas ; @Header() pose un en-tête de réponse, et les en-têtes de requête se lisent avec @Headers(),
  • ordre dans une méthode : décorateur, nom, corps, return this.service.findAll(),
  • pas de logique métier dans un contrôleur - ainsi tu peux aussi l'appeler hors HTTP,
  • NestJS choisit le code de statut : 200, et 201 pour @Post.

Dans la prochaine leçon, tu rencontreras ceux à qui le centurion transmet les ordres - les services, là où vit la vraie logique. En attendant, retiens ceci : un contrôleur reconnaît une requête et désigne son exécutant - et chaque donnée dont il a besoin arrive par l'un de trois chemins : le chemin d'adresse, la requête, ou le corps.

Code de cette leçon : src/legion.controller.ts
1// Contrôleurs - Centurions de la structure de l'Imperium
2import {
3  Controller, Get, Post, Put, Delete,
4  Body, Param, Query, HttpCode, Header,
5  ParseIntPipe, DefaultValuePipe,
6} from '@nestjs/common';
7
8console.log("Contrôleurs NestJS - centurions recevant les ordres !");
9
10// ===========================================
11// 1. Contrôleur de base avec décorateurs HTTP
12// ===========================================
13
14interface Legionary {
15  id: number;
16  name: string;
17  rank: string;
18  experience: number;
19}
20
21@Controller('legiones')
22export class LegionController {
23  private legionaries: Legionary[] = [
24    { id: 1, name: 'Marcus Aurelius', rank: 'Centurio', experience: 10 },
25    { id: 2, name: 'Julia Domna', rank: 'Optio', experience: 8 },
26    { id: 3, name: 'Titus Flavius', rank: 'Miles', experience: 3 },
27  ];
28
29  // GET /legiones
30  @Get()
31  findAll(
32    @Query('rank') rank?: string,
33    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit?: number,
34  ) {
35    let result = this.legionaries;
36    if (rank) {
37      result = result.filter(l => l.rank === rank);
38    }
39    return { legionaries: result.slice(0, limit), total: result.length };
40  }
41
42  // GET /legiones/:id
43  @Get(':id')
44  findOne(@Param('id', ParseIntPipe) id: number) {
45    const legionary = this.legionaries.find(l => l.id === id);
46    if (!legionary) {
47      return { error: 'Legionary non inventus !' };
48    }
49    return legionary;
50  }
51
52  // POST /legiones
53  @Post()
54  @HttpCode(201)
55  create(@Body() data: Omit<Legionary, 'id'>) {
56    const newLegionary: Legionary = {
57      id: Math.max(...this.legionaries.map(l => l.id)) + 1,
58      ...data,
59    };
60    this.legionaries.push(newLegionary);
61    return { message: 'Légionnaire recruté !', legionary: newLegionary };
62  }
63
64  // PUT /legiones/:id
65  @Put(':id')
66  update(@Param('id', ParseIntPipe) id: number, @Body() data: Partial<Legionary>) {
67    const index = this.legionaries.findIndex(l => l.id === id);
68    if (index === -1) return { error: 'Legionary non inventus !' };
69    this.legionaries[index] = { ...this.legionaries[index], ...data };
70    return { message: 'Données mises à jour !', legionary: this.legionaries[index] };
71  }
72
73  // DELETE /legiones/:id
74  @Delete(':id')
75  @HttpCode(204)
76  remove(@Param('id', ParseIntPipe) id: number) {
77    this.legionaries = this.legionaries.filter(l => l.id !== id);
78  }
79
80  // GET /legiones/search/:name
81  @Get('search/:name')
82  @Header('X-Imperium', 'Roma-Aeterna')
83  search(@Param('name') name: string) {
84    return this.legionaries.filter(
85      l => l.name.toLowerCase().includes(name.toLowerCase())
86    );
87  }
88}
89
90console.log("\n=== RÉCAPITULATIF DES CONTRÔLEURS ===");
91console.log("@Controller('prefix') - définit le préfixe de route");
92console.log("@Get, @Post, @Put, @Delete - méthodes HTTP");
93console.log("@Param, @Query, @Body - extraction des données de la requête");
94console.log("@HttpCode, @Header - configuration de la réponse");
95

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Comment extraire un paramètre d'URL dans un contrôleur NestJS (par exemple /legion/5) ?

  2. 2. Quel décorateur HTTP de NestJS sert à créer une nouvelle ressource ?

  3. 3. Comment récupérer un paramètre de requête (par exemple ?from=rome) dans un contrôleur NestJS ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Écris un contrôleur TributesController avec le décorateur @Controller('tributes') et quatre méthodes : findAll avec @Get(), create avec @Post(), update avec @Put(':id') et remove avec @Delete(':id').

  • Tri vertical

    Remets dans le bon ordre les éléments de la déclaration du contrôleur :

  • Clique dans l'ordre

    Remets dans le bon ordre les éléments de la méthode GET du contrôleur :

  • Éditeur de code

    Écris un contrôleur LegionController avec le décorateur @Controller('legion') et deux points de terminaison GET : search avec @Get('search'), qui lit @Query('province') province et renvoie { province }, et findOne avec @Get(':id'), qui lit @Param('id') id et renvoie { id }. Déclare search avant :id, car la route avec paramètre intercepterait l'adresse /legion/search.