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

Le routage et les méthodes HTTP (@Get, @Post, @Put, @Patch, @Delete)

Cette leçon détaille le routage dans NestJS : décorateurs @Get, @Post, @Put, @Patch, @Delete, @All, construction des chemins à partir du préfixe du contrôleur, CRUD complet d'une ressource avec son se...

0 chapitres 0 QCM Accès gratuit
Le routage et les méthodes HTTP (@Get, @Post, @Put, @Patch, @Delete)

À propos de ce cours

Cette leçon détaille le routage dans NestJS : décorateurs @Get, @Post, @Put, @Patch, @Delete, @All, construction des chemins à partir du préfixe du contrôleur, CRUD complet d'une ressource avec son service, différence entre PUT et PATCH, plusieurs chemins pour une même méthode, jokers nommés de NestJS 11, réponses automatiques aux requêtes HEAD et piège de l'ordre des routes (une route statique placée après une route à paramètre n'est jamais atteinte). Toutes les requêtes sont exécutées sur une vraie application et suivies d'activités corrigées.

Objectifs de la leçon

  • Associer chaque méthode HTTP au bon décorateur NestJS.
  • Écrire le CRUD complet d'une ressource.
  • Choisir entre PUT et PATCH.
  • Éviter le piège de l'ordre des routes.
  • Utiliser plusieurs chemins et les jokers.
Les décorateurs de méthode HTTP
DécorateurMéthode HTTPUsage
@Get()GETlire
@Post()POSTcréer (ou déclencher une action)
@Put()PUTremplacer entièrement
@Patch()PATCHmodifier partiellement
@Delete()DELETEsupprimer
@All()toutesrarement utile (journalisation, proxy)
@Head(), @Options()HEAD, OPTIONScas particuliers

Le chemin complet d'une route est la concaténation de : préfixe global (/api) + préfixe du contrôleur (/formations) + chemin du décorateur (/:id).

Routes CRUD d'une ressource NestJS

Une ressource, cinq routes : lister, afficher, créer, remplacer ou modifier, supprimer.

Un CRUD complet

Voici la ressource formations complète, avec ses données en mémoire (la base de données arrivera au chapitre 11). Le service contient toute la logique :

📄 src/formations/formations.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';

export interface Formation {
  id: number;
  intitule: string;
  heures: number;
  publiee: boolean;
}

@Injectable()
export class FormationsService {
  private formations: Formation[] = [
    { id: 1, intitule: 'Développement digital', heures: 1200, publiee: true },
    { id: 2, intitule: 'Infrastructure digitale', heures: 1100, publiee: false },
  ];
  private prochainId = 3;

  findAll(): Formation[] {
    return this.formations;
  }

  findPubliees(): Formation[] {
    return this.formations.filter((f) => f.publiee);
  }

  findOne(id: number): Formation {
    const f = this.formations.find((x) => x.id === id);
    if (!f) throw new NotFoundException(`Formation ${id} introuvable`);
    return f;
  }

  create(donnees: Omit<Formation, 'id'>): Formation {
    const f = { id: this.prochainId++, ...donnees };
    this.formations.push(f);
    return f;
  }

  replace(id: number, donnees: Omit<Formation, 'id'>): Formation {
    const index = this.formations.indexOf(this.findOne(id));
    this.formations[index] = { id, ...donnees };          // remplacement complet
    return this.formations[index];
  }

  update(id: number, donnees: Partial<Omit<Formation, 'id'>>): Formation {
    return Object.assign(this.findOne(id), donnees);       // modification partielle
  }

  remove(id: number): Formation {
    const f = this.findOne(id);
    this.formations = this.formations.filter((x) => x.id !== id);
    return f;
  }
}

Le contrôleur associe chaque route à une méthode du service :

📄 src/formations/formations.controller.ts

import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
import { Formation, FormationsService } from './formations.service';

@Controller('formations')
export class FormationsController {
  constructor(private readonly formationsService: FormationsService) {}

  @Get()                                   // GET /api/formations
  findAll() {
    return this.formationsService.findAll();
  }

  @Get('publiees')                         // GET /api/formations/publiees  (AVANT ':id' !)
  findPubliees() {
    return this.formationsService.findPubliees();
  }

  @Get(['catalogue', 'offre'])             // deux chemins pour la même méthode
  catalogue() {
    return this.formationsService.findPubliees().map((f) => f.intitule);
  }

  @Get(':id')                              // GET /api/formations/2
  findOne(@Param('id') id: string) {
    return this.formationsService.findOne(Number(id));
  }

  @Post()                                  // POST /api/formations
  create(@Body() donnees: Omit<Formation, 'id'>) {
    return this.formationsService.create(donnees);
  }

  @Put(':id')                              // PUT /api/formations/2 : remplace tout
  replace(@Param('id') id: string, @Body() donnees: Omit<Formation, 'id'>) {
    return this.formationsService.replace(Number(id), donnees);
  }

  @Patch(':id')                            // PATCH /api/formations/2 : modifie une partie
  update(@Param('id') id: string, @Body() donnees: Partial<Omit<Formation, 'id'>>) {
    return this.formationsService.update(Number(id), donnees);
  }

  @Delete(':id')                           // DELETE /api/formations/2
  remove(@Param('id') id: string) {
    return this.formationsService.remove(Number(id));
  }
}

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

GET /api/formations → 200 [{"id":1,"intitule":"Développement digital","heures":1200,"publiee":true},{"id":2,"intitule":"Infrastructure digitale","heures":1100,"publiee":false}]
GET /api/formations/publiees → 200 [{"id":1,"intitule":"Développement digital","heures":1200,"publiee":true}]
GET /api/formations/offre → 200 ["Développement digital"]
GET /api/formations/2 → 200 {"id":2,"intitule":"Infrastructure digitale","heures":1100,"publiee":false}
POST /api/formations → 201 {"id":3,"intitule":"Réseaux","heures":900,"publiee":false}
PATCH /api/formations/3 → 200 {"id":3,"intitule":"Réseaux","heures":900,"publiee":true}
PUT /api/formations/3 → 200 {"id":3,"intitule":"Réseaux et cybersécurité","heures":950,"publiee":true}
DELETE /api/formations/3 → 200 {"id":3,"intitule":"Réseaux et cybersécurité","heures":950,"publiee":true}
GET /api/formations/3 → 404 {"message":"Formation 3 introuvable","error":"Not Found","statusCode":404}
PUT /api/formations → 404 {"message":"Cannot PUT /api/formations","error":"Not Found","statusCode":404}
HEAD /api/formations/1 → 200

Points à observer :

  • Le POST répond 201, les autres 200.
  • Après le DELETE, la formation 3 n'existe plus : 404, grâce à NotFoundException levée dans le service.
  • PUT /api/formations (sans identifiant) : aucune route ne correspond à cette méthode et ce chemin, NestJS répond 404 « Cannot PUT ».
  • Une requête HEAD est servie automatiquement par la route GET correspondante : mêmes en-têtes, sans corps.
PUT ou PATCH ?
PUTPATCH
Sensremplacer la ressource entièremodifier certains champs
Corps envoyéla ressource complèteseulement les champs modifiés
Champ absent du corpsdisparaît (ou erreur de validation)inchangé
Exemple{ intitule, heures, publiee }{ publiee: true }

Dans la pratique, la plupart des API (et le générateur nest g resource) n'utilisent que PATCH pour les modifications.

Le piège de l'ordre des routes

NestJS (comme Express) teste les routes d'un contrôleur dans l'ordre de déclaration et s'arrête à la première qui correspond. Un paramètre comme :code accepte n'importe quelle valeur… y compris actifs:

📄 src/groupes.controller.ts

import { Controller, Get, Param } from '@nestjs/common';

@Controller('groupes')
export class GroupesController {
  @Get(':code')                            // ✗ déclarée en premier : elle capture aussi « actifs »
  findOne(@Param('code') code: string) {
    return `groupe ${code}`;
  }

  @Get('actifs')                           // jamais atteinte !
  findActifs() {
    return 'liste des groupes actifs';
  }
}

Réponses obtenues :

GET /api/groupes/DEV101 → 200 groupe DEV101
GET /api/groupes/actifs → 200 groupe actifs

La route actifs n'est jamais atteinte : « actifs » est pris pour un code de groupe. Règle: déclarez toujours les routes statiques (publiees, actifs, statistiques) avant les routes à paramètre (:id). C'est ce que fait le contrôleur des formations ci-dessus.

Plusieurs chemins et jokers

Un décorateur accepte un tableau de chemins : @Get(['catalogue', 'offre']) (voir la réponse à /api/formations/offre ci-dessus).

Pour capturer « tout le reste du chemin », NestJS 11 utilise un joker nommé: *nom. La valeur reçue est un tableau de segments.

📄 src/documents.controller.ts

import { Controller, Get, Param } from '@nestjs/common';

@Controller('documents')
export class DocumentsController {
  @Get('*chemin')                          // joker nommé : capture tout le reste du chemin
  lire(@Param('chemin') chemin: string[]) {
    return { segments: chemin, fichier: chemin.at(-1), dossier: chemin.slice(0, -1).join('/') };
  }
}

Réponse obtenue :

GET /api/documents/cours/nestjs/chapitre-06.pdf → 200 {"segments":["cours","nestjs","chapitre-06.pdf"],"fichier":"chapitre-06.pdf","dossier":"cours/nestjs"}

Changement de NestJS 11: avec Express 5, l'ancienne écriture @Get('fichiers/*') ou 'ab*cd' n'est plus acceptée de la même façon. Il faut nommer le joker (*chemin) ; pour qu'il accepte aussi un chemin vide, on place la barre et le joker entre accolades : @Get('fichiers{/*chemin}') (et non 'fichiers/{*chemin}', qui exigerait la barre finale). Les tutoriels écrits pour les versions précédentes utilisent souvent l'ancienne syntaxe.

Activités

Activité 1 Associer requêtes et routes

Avec le contrôleur des formations ci-dessus, quelle méthode est appelée (ou quel statut est renvoyé) pour : (a) GET /api/formations/catalogue; (b) PATCH /api/formations; (c) DELETE /api/formations/1; (d) GET /api/formations/abc; (e) POST /api/formations/1?

Voir le corrigé

(a) catalogue(); (b) 404 « Cannot PATCH /api/formations » (le PATCH attend un :id) ; (c) remove('1'), statut 200 ; (d) findOne('abc'): Number('abc') vaut NaN, aucune formation ne correspond, donc 404 « Formation NaN introuvable ». Ce message maladroit sera évité avec ParseIntPipe (leçon 6.3), qui renvoie 400 ; (e) 404 : aucun POST avec identifiant.

Activité 2 Corriger l'ordre des routes

Recopiez le GroupesController défectueux et vérifiez que /api/groupes/actifs renvoie « groupe actifs ». Corrigez-le, puis ajoutez une route GET /api/groupes/statistiques/effectifs. Faut-il la placer avant :code?

Voir le corrigé

Il suffit d'inverser les deux méthodes. Pour statistiques/effectifs (deux segments), l'ordre n'a pas d'importance face à :code, qui ne correspond qu'à un seul segment. Par sécurité et pour la lisibilité, gardez néanmoins toutes les routes statiques en premier.

Activité 3 Le CRUD des salles

Écrivez le CRUD complet d'une ressource salles ({ id, code, places, equipee: boolean }) dans un module SallesModule: lister, afficher, créer, modifier (PATCH), supprimer, plus GET /api/salles/equipees. Testez chaque route avec un fichier requetes/salles.http.

Voir le corrigé

Reprenez la structure de FormationsService et FormationsController en remplaçant les noms, sans oublier : equipees déclarée avant :id, et NotFoundException dans findOne, réutilisée par update et remove. Les statuts attendus sont 200, 201 pour le POST et 404 pour un identifiant inexistant.

Activité 4 PUT contre PATCH

Avec le contrôleur des formations, envoyez PUT /api/formations/1 avec seulement { "heures": 1300 }. Que devient la formation ? Envoyez le même corps en PATCH sur la formation 2. Concluez.

Voir le corrigé

Avec PUT, la formation 1 devient { "id": 1, "heures": 1300 }: intitulé et état de publication ont disparu, car PUT remplace tout. Avec PATCH, seule la durée de la formation 2 change. Sans validation (chapitre 08), rien n'empêche un client d'envoyer un PUT incomplet : c'est une raison de plus de préférer PATCH.

Activité 5 Un joker utile

Ajoutez au contrôleur des documents une route GET /api/documents/telecharger/… (chemin facultatif) qui renvoie { chemin: 'a/b/c.pdf', extension: 'pdf' }, et { chemin: '', extension: null } pour /api/documents/telecharger. Attention à l'ordre avec la route *chemin existante.

Voir le corrigé
@Get('telecharger{/*chemin}')              // AVANT '*chemin', sinon jamais atteinte
telecharger(@Param('chemin') chemin: string[] = []) {
  const complet = chemin.join('/');
  const extension = complet.includes('.') ? complet.split('.').pop() : null;
  return { chemin: complet, extension };
}

Les accolades rendent facultatif tout ce qu'elles contiennent, barre comprise: 'telecharger{/*chemin}' accepte /telecharger, /telecharger/ et /telecharger/a/b/c.pdf (vérifié). Avec 'telecharger/{*chemin}', la requête /telecharger sans barre finale ne correspondrait pas et tomberait sur la route *chemin. La valeur par défaut = [] couvre le cas où le joker est absent.

À retenir

  • @Get, @Post, @Put, @Patch, @Delete : chemin = préfixe global + contrôleur + décorateur.
  • CRUD : GET liste, GET :id, POST, PATCH :id (ou PUT), DELETE :id.
  • PUT remplace tout, PATCH modifie ce qui est envoyé : préférer PATCH.
  • Les routes sont testées dans l'ordre : routes statiques avant les routes à paramètre.
  • NestJS 11 : jokers nommés *chemin (tableau de segments), {/*chemin} pour un joker facultatif.