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

Paramètres de route, query strings et en-têtes (@Param, @Query, @Headers)

Cette leçon explique comment lire les données transmises dans l'URL et les en-têtes d'une requête : paramètres de route avec @Param (un ou plusieurs, ou tous dans un objet), conversion avec ParseIntPi...

0 chapitres 0 QCM Accès gratuit
Paramètres de route, query strings et en-têtes (@Param, @Query, @Headers)

À propos de ce cours

Cette leçon explique comment lire les données transmises dans l'URL et les en-têtes d'une requête : paramètres de route avec @Param (un ou plusieurs, ou tous dans un objet), conversion avec ParseIntPipe, query string avec @Query pour filtrer, rechercher, trier et paginer une liste, valeurs par défaut avec DefaultValuePipe, booléens avec ParseBoolPipe, valeurs multiples, limites du format de la query string avec Express 5, en-têtes avec @Headers et adresse IP avec @Ip. Un contrôleur complet est testé avec de nombreuses requêtes, suivies d'activités corrigées.

Objectifs de la leçon

  • Lire un ou plusieurs paramètres de route avec @Param.
  • Convertir et valider un identifiant avec ParseIntPipe.
  • Filtrer, trier et paginer une liste avec @Query.
  • Donner des valeurs par défaut et convertir des booléens.
  • Lire des en-têtes avec @Headers.

Sources de données d'une requête HTTP

Paramètres de route, query string, en-têtes et corps : un décorateur pour chaque source.

Paramètre de route ou query string ?
Paramètre de routeQuery string
Où ?dans le chemin : /stagiaires/3après le ?: /stagiaires?groupe=DEV101
Rôleidentifier une ressourceaffiner une liste : filtre, recherche, tri, pagination
Obligatoire ?oui (sinon la route ne correspond pas)non, en général
Décorateur@Param('id')@Query('groupe')

Une règle simple : si la réponse est une ressource précise, c'est un paramètre de route ; si c'est une liste filtrée, ce sont des paramètres de requête.

Le contrôleur d'exemple

📄 src/stagiaires.controller.ts

import {
  Controller, DefaultValuePipe, Get, Headers, Ip, Param, ParseBoolPipe, ParseIntPipe, Query,
} from '@nestjs/common';

const STAGIAIRES = [
  { id: 1, nom: 'Ben Ali', prenom: 'Sami', groupe: 'DEV101', moyenne: 14, actif: true },
  { id: 2, nom: 'Trabelsi', prenom: 'Ines', groupe: 'DEV101', moyenne: 16.7, actif: true },
  { id: 3, nom: 'Gharbi', prenom: 'Yassine', groupe: 'DEV102', moyenne: 9.3, actif: false },
  { id: 4, nom: 'Mansouri', prenom: 'Lina', groupe: 'DEV102', moyenne: 13, actif: true },
  { id: 5, nom: 'Jlassi', prenom: 'Omar', groupe: 'INF101', moyenne: 11.5, actif: true },
];

@Controller()
export class StagiairesController {
  // 1. Paramètre de route
  @Get('stagiaires/:id')
  findOne(@Param('id', ParseIntPipe) id: number) {          // « 3 » → 3, sinon 400
    return STAGIAIRES.find((s) => s.id === id) ?? { message: `Stagiaire ${id} introuvable` };
  }

  // 2. Plusieurs paramètres de route
  @Get('groupes/:code/stagiaires/:id')
  dansGroupe(@Param('code') code: string, @Param('id', ParseIntPipe) id: number) {
    const s = STAGIAIRES.find((x) => x.id === id && x.groupe === code.toUpperCase());
    return s ?? { message: `Aucun stagiaire ${id} dans le groupe ${code}` };
  }

  // 3. Tous les paramètres d'un coup
  @Get('debug/:a/:b')
  tous(@Param() params: Record<string, string>) {
    return params;
  }

  // 4. Query string : filtres, recherche, tri, pagination
  @Get('stagiaires')
  findAll(
    @Query('groupe') groupe?: string | string[],
    @Query('q') recherche?: string,
    @Query('actif', new DefaultValuePipe(true), ParseBoolPipe) actif?: boolean,
    @Query('tri', new DefaultValuePipe('nom')) tri?: 'nom' | 'moyenne',
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page = 1,
    @Query('parPage', new DefaultValuePipe(2), ParseIntPipe) parPage = 2,
  ) {
    const groupes = groupe === undefined ? [] : ([] as string[]).concat(groupe).map((g) => g.toUpperCase());
    let liste = STAGIAIRES.filter((s) => s.actif === actif);
    if (groupes.length) liste = liste.filter((s) => groupes.includes(s.groupe));
    if (recherche) liste = liste.filter((s) => `${s.nom} ${s.prenom}`.toLowerCase().includes(recherche.toLowerCase()));
    liste = liste.toSorted((a, b) => (tri === 'moyenne' ? b.moyenne - a.moyenne : a.nom.localeCompare(b.nom)));
    const debut = (page - 1) * parPage;
    return {
      total: liste.length,
      page,
      parPage,
      donnees: liste.slice(debut, debut + parPage).map((s) => `${s.prenom} ${s.nom} (${s.groupe}, ${s.moyenne})`),
    };
  }

  // 5. Toute la query string dans un objet
  @Get('recherche')
  recherche(@Query() query: Record<string, string | string[]>) {
    return query;
  }

  // 6. En-têtes et adresse IP
  @Get('client')
  infosClient(
    @Headers('accept-language') langue: string | undefined,
    @Headers('x-cle-api') cle: string | undefined,
    @Ip() ip: string,
  ) {
    return { langue: langue?.slice(0, 2) ?? 'fr', cleFournie: Boolean(cle), ipLocale: ip.includes('127.0.0.1') || ip === '::1' };
  }
}
1. Les paramètres de route

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

GET /api/stagiaires/3 → 200 {"id":3,"nom":"Gharbi","prenom":"Yassine","groupe":"DEV102","moyenne":9.3,"actif":false}
GET /api/stagiaires/abc → 400 {"message":"Validation failed (numeric string is expected)","error":"Bad Request","statusCode":400}
GET /api/groupes/dev101/stagiaires/2 → 200 {"id":2,"nom":"Trabelsi","prenom":"Ines","groupe":"DEV101","moyenne":16.7,"actif":true}
GET /api/debug/premier/second → 200 {"a":"premier","b":"second"}
  • Tout ce qui vient de l'URL est d'abord une chaîne. @Param('id', ParseIntPipe) la convertit en nombre… et renvoie automatiquement 400 si ce n'en est pas un : le contrôleur ne reçoit jamais « abc ».
  • Une route peut avoir plusieurs paramètres (groupes/:code/stagiaires/:id), chacun lu par son propre @Param.
  • @Param() sans argument renvoie un objet avec tous les paramètres.

Les pipes en avant-première: ParseIntPipe, ParseBoolPipe, ParseFloatPipe, DefaultValuePipe… sont des pipes intégrés à NestJS (leçon 5.5). Ils sont étudiés en détail au chapitre 08 ; on les utilise dès maintenant car ils évitent de nombreux bugs.

2. La query string : filtrer, rechercher, trier, paginer

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

GET /api/stagiaires → 200 {"total":4,"page":1,"parPage":2,"donnees":["Sami Ben Ali (DEV101, 14)","Omar Jlassi (INF101, 11.5)"]}
GET /api/stagiaires?page=2 → 200 {"total":4,"page":2,"parPage":2,"donnees":["Lina Mansouri (DEV102, 13)","Ines Trabelsi (DEV101, 16.7)"]}
GET /api/stagiaires?groupe=dev102&tri=moyenne → 200 {"total":1,"page":1,"parPage":2,"donnees":["Lina Mansouri (DEV102, 13)"]}
GET /api/stagiaires?groupe=DEV101&groupe=INF101&parPage=10 → 200 {"total":3,"page":1,"parPage":10,"donnees":["Sami Ben Ali (DEV101, 14)","Omar Jlassi (INF101, 11.5)","Ines Trabelsi (DEV101, 16.7)"]}
GET /api/stagiaires?q=man&actif=true → 200 {"total":1,"page":1,"parPage":2,"donnees":["Lina Mansouri (DEV102, 13)"]}
GET /api/stagiaires?actif=false → 200 {"total":1,"page":1,"parPage":2,"donnees":["Yassine Gharbi (DEV102, 9.3)"]}
GET /api/stagiaires?actif=peut-etre → 400 {"message":"Validation failed (boolean string is expected)","error":"Bad Request","statusCode":400}
  • Valeur absente: @Query('q') vaut undefined. new DefaultValuePipe(1) fournit une valeur par défaut, placé avant le pipe de conversion.
  • Booléens: ?actif=false donne la chaîne 'false'… qui est « vraie » en JavaScript ! ParseBoolPipe la convertit correctement et refuse les autres valeurs (400 pour « peut-etre »).
  • Valeurs multiples: ?groupe=DEV101&groupe=INF101 donne un tableau, alors que ?groupe=DEV101 donne une chaîne. D'où le type string | string[] et la normalisation [].concat(groupe).
  • Pagination: page et parPage, avec le total renvoyé pour que le client puisse afficher le nombre de pages.
3. Toute la query string dans un objet

Réponse obtenue :

GET /api/recherche?tag=nestjs&tag=api&niveau=2&filtre[groupe]=DEV101 → 200 {"tag":["nestjs","api"],"niveau":"2","filtre[groupe]":"DEV101"}

@Query() sans argument renvoie un objet avec toutes les valeurs, toujours sous forme de chaînes (niveau vaut "2"). Remarquez aussi que filtre[groupe]=DEV101 n'est pas transformé en objet imbriqué : depuis Express 5 (utilisé par NestJS 11), la query string est analysée en mode « simple ». Préférez des noms plats (?groupe=DEV101) ; au chapitre 08, un DTO validé transformera ces chaînes en vrais types.

4. Les en-têtes et l'adresse IP

Réponse (requête envoyée avec Accept-Language: ar-TN et X-Cle-Api: secret) :

GET /api/client → 200 {"langue":"ar","cleFournie":true,"ipLocale":true}
  • @Headers('accept-language') lit un en-tête. Les noms d'en-têtes sont insensibles à la casse; écrivez-les en minuscules dans @Headers.
  • @Headers() sans argument donne l'objet de tous les en-têtes.
  • @Ip() donne l'adresse IP du client (derrière un proxy comme Nginx, il faudra configurer Express pour lire l'en-tête X-Forwarded-For: chapitre 20).

Ne mettez jamais de données sensibles (mot de passe, jeton) dans l'URL : elle est enregistrée dans les journaux des serveurs et l'historique du navigateur. Les jetons voyagent dans un en-tête (Authorization), les mots de passe dans le corps d'un POST.

Activités

Activité 1 Route ou query ?

Pour chaque besoin, proposez l'URL : (a) le groupe DEV102 ; (b) les groupes de la formation 1 ; (c) les stagiaires dont le nom contient « ben », triés par moyenne ; (d) les absences du stagiaire 7 ; (e) la page 3 des absences du stagiaire 7, 20 par page ; (f) les notes du stagiaire 7 en NestJS.

Voir le corrigé

(a) /api/groupes/DEV102; (b) /api/groupes?formation=1 (ou /api/formations/1/groupes) ; (c) /api/stagiaires?q=ben&tri=moyenne; (d) /api/stagiaires/7/absences; (e) /api/stagiaires/7/absences?page=3&parPage=20; (f) /api/stagiaires/7/notes?matiere=nestjs.

Activité 2 Sécuriser un identifiant

Dans le CRUD des formations (leçon 6.2), remplacez les Number(id) par ParseIntPipe. Comparez la réponse à GET /api/formations/abc avant et après. Que devient le type du paramètre dans la signature ?

Voir le corrigé

Avant : 404 « Formation NaN introuvable » (message trompeur). Après : 400 « Validation failed (numeric string is expected) ». La signature devient findOne(@Param('id', ParseIntPipe) id: number): le paramètre est un vrai nombre, plus besoin de conversion dans le contrôleur.

Activité 3 Filtres booléens

Ajoutez au GET /api/formations un filtre facultatif ?publiee=true|false. Sans filtre, toutes les formations sont renvoyées. Pourquoi ne pas utiliser DefaultValuePipe ici ? Comment rendre ParseBoolPipe facultatif ?

Voir le corrigé
@Get()
findAll(@Query('publiee', new ParseBoolPipe({ optional: true })) publiee?: boolean) {
  const liste = this.formationsService.findAll();
  return publiee === undefined ? liste : liste.filter((f) => f.publiee === publiee);
}

Une valeur par défaut imposerait un filtre alors qu'on veut « tout » quand le paramètre est absent. L'option optional: true laisse passer undefined sans erreur.

Activité 4 Pagination réutilisable

Écrivez dans le service une méthode générique paginer<T>(liste: T[], page: number, parPage: number) qui renvoie { total, page, parPage, pages, donnees }, et utilisez-la dans GET /api/formations?page=&parPage=. Limitez parPage à 50 au maximum.

Voir le corrigé
paginer<T>(liste: T[], page: number, parPage: number) {
  const taille = Math.min(Math.max(parPage, 1), 50);
  const pages = Math.max(Math.ceil(liste.length / taille), 1);
  const courante = Math.min(Math.max(page, 1), pages);
  const debut = (courante - 1) * taille;
  return { total: liste.length, page: courante, parPage: taille, pages, donnees: liste.slice(debut, debut + taille) };
}

// contrôleur
@Get()
findAll(
  @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
  @Query('parPage', new DefaultValuePipe(10), ParseIntPipe) parPage: number,
) {
  return this.formationsService.paginer(this.formationsService.findAll(), page, parPage);
}

Les bornes protègent l'API : un client qui demande parPage=100000 ne fera pas charger toute la base. Le générique <T> (leçon 4.6) permet de réutiliser la méthode pour toutes les ressources.

Activité 5 Une clé d'API dans un en-tête

Ajoutez une route GET /api/formations/export qui exige l'en-tête X-Cle-Api égal à formation-2026 et renvoie 401 sinon. Où ce contrôle devrait-il être placé dans une vraie application ?

Voir le corrigé
@Get('export')
exporter(@Headers('x-cle-api') cle?: string) {
  if (cle !== 'formation-2026') throw new UnauthorizedException('Clé d\'API absente ou invalide');
  return this.formationsService.findAll();
}

Ce contrôle d'accès devrait être dans un guard (chapitres 09 et 13), réutilisable sur plusieurs routes, et la clé dans le fichier .env (chapitre 10), jamais écrite dans le code.

À retenir

  • Paramètre de route (@Param) pour identifier une ressource, query (@Query) pour affiner une liste.
  • Tout ce qui vient de l'URL est une chaîne : ParseIntPipe, ParseBoolPipe convertissent et renvoient 400 si besoin.
  • DefaultValuePipe fournit une valeur par défaut ; { optional: true } accepte l'absence.
  • Un paramètre répété donne un tableau ; les noms imbriqués (a[b]) ne sont pas analysés par Express 5.
  • @Headers('nom-en-minuscules') lit un en-tête ; jamais de secret dans l'URL.