Skip to content

3. Le modèle d'une action

Une action a besoin des informations envoyées par le navigateur : les paramètres de l'URL ([?nom=martin&age=42]), les champs d'un formulaire, les paramètres de route. Le cours d'origine appelle modèle de l'action l'ensemble de ces informations. Ce chapitre montre comment [NestJS] les fournit à l'action, les convertit dans le bon type et les valide.

3.1. Exemple 07 : les paramètres de l’URL

Le fichier [src/parametres.controller.ts] :

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

@Controller('parametres')
export class ParametresController {
  // /parametres/action01?nom=martin
  @Get('action01')
  action01(@Query('nom') nom: string): string {
    return `nom = [${nom}], de type ${typeof nom}`;
  }

  // /parametres/action02?age=42 : un paramètre est TOUJOURS une chaîne
  @Get('action02')
  action02(@Query('age') age: number): string {
    return `age = [${age}], de type ${typeof age}`;
  }

  // /parametres/action03?age=42 : ParseIntPipe convertit (ou refuse)
  @Get('action03')
  action03(@Query('age', ParseIntPipe) age: number): string {
    return `age = [${age}], de type ${typeof age}`;
  }

  // /parametres/action04?page=2 : une valeur par défaut si le paramètre manque
  @Get('action04')
  action04(@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number): string {
    return `page = [${page}], de type ${typeof page}`;
  }

  // /parametres/action05?nom=martin&age=42 : tous les paramètres d'un coup
  @Get('action05')
  action05(@Query() parametres: Record<string, string>): object {
    return parametres;
  }
}

Commentons ce code :

  • lignes 6-9 : [@Query('nom')] injecte la valeur du paramètre [nom] de l'URL (la partie qui suit le [?], appelée chaîne de requête ou [query string]) ;
  • lignes 12-15 : le paramètre [age] est déclaré de type [number]... mais ce type n'est qu'une déclaration [TypeScript], effacée à la compilation. Ce qu'on reçoit est toujours une chaîne, comme le montre l'exécution ;
  • lignes 18-21 : [ParseIntPipe] est un pipe : un objet qui transforme (et vérifie) la valeur d'un paramètre avant qu'elle n'arrive à l'action. Ici, la chaîne ['42'] devient le nombre 42 ; une chaîne non numérique provoque une erreur 400 (Bad Request), sans que l'action soit exécutée ;
  • lignes 24-27 : les pipes s'enchaînent : [DefaultValuePipe(1)] remplace un paramètre absent par 1, puis [ParseIntPipe] convertit ;
  • lignes 30-33 : [@Query()] sans argument injecte un objet contenant tous les paramètres de l'URL.

Les résultats :

curl -i "http://localhost:3000/parametres/action01?nom=martin"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 30

nom = [martin], de type string
curl -i "http://localhost:3000/parametres/action01"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 36

nom = [undefined], de type undefined
curl -i "http://localhost:3000/parametres/action02?age=42"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 26

age = [42], de type string
curl -i "http://localhost:3000/parametres/action03?age=42"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 26

age = [42], de type number
curl -i "http://localhost:3000/parametres/action03?age=abc"
1
2
3
4
5
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 99

{"message":"Validation failed (numeric string is expected)","error":"Bad Request","statusCode":400}
curl -i "http://localhost:3000/parametres/action04"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 26

page = [1], de type number
curl -i "http://localhost:3000/parametres/action04?page=2"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 26

page = [2], de type number
curl -i "http://localhost:3000/parametres/action05?nom=martin&age=42"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 27

{"nom":"martin","age":"42"}

Les guillemets autour de l'URL sont nécessaires quand elle contient le caractère [&], qui a un sens particulier pour la console. On retiendra :

  • un paramètre absent vaut [undefined] ;
  • un paramètre présent est une chaîne, quel que soit le type déclaré ;
  • un pipe permet de convertir et de valider un paramètre, et répond 400 si la valeur est invalide.

Image

3.2. Exemple 08 : un modèle d’action décrit par une classe

Quand une action a plusieurs paramètres, on les décrit par une classe, souvent appelée [DTO] ([Data Transfer Object]). Chaque champ de la classe porte des décorateurs de validation, fournis par la bibliothèque [class-validator]. C'est l'équivalent des attributs [[Required]], [[Range]]... d'[ASP.NET MVC].

Le fichier [src/personne.dto.ts] :

import { Type } from 'class-transformer';
import { IsEmail, IsIn, IsInt, IsNotEmpty, IsOptional, Length, Max, Min } from 'class-validator';

export class PersonneDto {
  @IsNotEmpty({ message: 'le nom est obligatoire' })
  @Length(2, 20, { message: 'le nom doit avoir de 2 à 20 caractères' })
  nom: string;

  @Type(() => Number)
  @IsInt({ message: "l'âge doit être un nombre entier" })
  @Min(0, { message: "l'âge doit être positif" })
  @Max(150, { message: "l'âge ne peut dépasser 150" })
  age: number;

  @IsIn(['Mr', 'Mme', 'Melle'], { message: 'titre inconnu' })
  titre: string;

  @IsOptional()
  @IsEmail({}, { message: 'adresse électronique invalide' })
  email?: string;
}

Commentons ce code :

  • ligne 1 : [@Type] vient de la bibliothèque [class-transformer], qui convertit les données reçues ;
  • ligne 2 : les décorateurs de validation de [class-validator] ;
  • lignes 5-7 : le nom est obligatoire ([@IsNotEmpty]) et doit avoir de 2 à 20 caractères ([@Length]). Chaque décorateur accepte un message d'erreur personnalisé ;
  • lignes 9-13 : [@Type(() => Number)] demande la conversion de la chaîne reçue en nombre, avant la validation : un entier ([@IsInt]) compris entre 0 et 150 ([@Min], [@Max]) ;
  • lignes 15-16 : le titre doit appartenir à une liste de valeurs ;
  • lignes 18-20 : l'adresse électronique est facultative ([@IsOptional]) ; si elle est présente, elle doit être valide ([@IsEmail]).

Le fichier [src/personnes.controller.ts] :

import { Controller, Get, Query } from '@nestjs/common';
import { PersonneDto } from './personne.dto';

@Controller('personnes')
export class PersonnesController {
  // /personnes/action01?nom=martin&age=42&titre=Mr
  @Get('action01')
  action01(@Query() personne: PersonneDto): object {
    return {
      personne,
      typeAge: typeof personne.age,
      estUnePersonneDto: personne instanceof PersonneDto,
    };
  }
}

ligne 8 : l'action reçoit un [PersonneDto]. L'action rend la personne reçue, le type de son âge, et vérifie qu'il s'agit bien d'une instance de la classe [PersonneDto].

Pour que les décorateurs de la classe soient appliqués, il faut activer la validation, une fois pour toutes, dans [main.ts] :

Le fichier [src/main.ts] :

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // validation automatique de TOUS les modèles d'action décrits par une classe
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true, // convertit les chaînes reçues vers les types déclarés
      whitelist: true, // supprime les champs non déclarés dans la classe
    }),
  );
  await app.listen(3000);
  console.log('Serveur démarré sur http://localhost:3000');
}

bootstrap();
  • lignes 8-13 : un pipe global, [ValidationPipe], est appliqué à tous les paramètres de toutes les actions. Pour un paramètre dont le type est une classe, il vérifie les contraintes de ses décorateurs ;
  • ligne 10 : [transform: true] : l'action reçoit une vraie instance de [PersonneDto], dont les champs ont été convertis ([@Type]) ;
  • ligne 11 : [whitelist: true] : les paramètres qui ne correspondent à aucun champ de la classe sont supprimés.

Une requête valide, avec un paramètre [pirate] non déclaré dans la classe :

curl -i "http://localhost:3000/personnes/action01?nom=martin&age=42&titre=Mr&pirate=1"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 95

{"personne":{"nom":"martin","age":42,"titre":"Mr"},"typeAge":"number","estUnePersonneDto":true}

L'âge est bien devenu un nombre, et le paramètre [pirate] a disparu. Une requête invalide :

curl -i "http://localhost:3000/personnes/action01?nom=m&age=abc&titre=Dr&email=xx"
1
2
3
4
5
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 240

{"message":["le nom doit avoir de 2 à 20 caractères","l'âge ne peut dépasser 150","l'âge doit être positif","l'âge doit être un nombre entier","titre inconnu","adresse électronique invalide"],"error":"Bad Request","statusCode":400}

[ValidationPipe] a refusé la requête avec une erreur 400, dont le corps JSON liste les messages d'erreur. L'action n'a pas été exécutée. Même chose sans aucun paramètre :

curl -i http://localhost:3000/personnes/action01
1
2
3
4
5
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 232

{"message":["le nom doit avoir de 2 à 20 caractères","le nom est obligatoire","l'âge ne peut dépasser 150","l'âge doit être positif","l'âge doit être un nombre entier","titre inconnu"],"error":"Bad Request","statusCode":400}

Ce comportement convient à un serveur JSON, interrogé par un client [JavaScript] qui affiche les erreurs à sa façon. Pour une application HTML, un JSON d'erreur n'a aucun sens : on veut réafficher le formulaire avec ses erreurs. L'exemple suivant montre comment garder la main.

3.3. Exemple 09 : la validation faite par l’action

L'exemple 09 n'a pas de [ValidationPipe] global. C'est l'action qui convertit et valide elle-même le modèle, avec les mêmes décorateurs (le fichier [personne.dto.ts] est identique) :

Le fichier [src/personnes.controller.ts] :

import { Controller, Get, Query } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { PersonneDto } from './personne.dto';

@Controller('personnes')
export class PersonnesController {
  // pas de ValidationPipe ici : l'action reçoit les paramètres BRUTS et
  // décide elle-même de ce qu'elle fait des erreurs
  @Get('action01')
  async action01(@Query() brut: Record<string, string>): Promise<string> {
    // 1. objet brut -> instance de PersonneDto (conversions @Type comprises)
    const personne = plainToInstance(PersonneDto, brut);
    // 2. vérification des contraintes (@IsNotEmpty, @Min...)
    const erreurs = await validate(personne);
    // 3. une page HTML dans les deux cas : pas d'erreur 400
    if (erreurs.length === 0) {
      return `<p>Bienvenue ${personne.titre} ${personne.nom} (${personne.age} ans)</p>`;
    }
    const items = erreurs
      .map((e) => `<li>${e.property} : ${Object.values(e.constraints ?? {}).join(', ')}</li>`)
      .join('');
    return `<p>Le formulaire contient des erreurs :</p><ul>${items}</ul>`;
  }
}

Commentons ce code :

  • ligne 11 : l'action reçoit les paramètres bruts : un objet dont tous les champs sont des chaînes ;
  • ligne 13 : [plainToInstance] ([class-transformer]) fabrique une instance de [PersonneDto] à partir de l'objet brut, en appliquant les conversions ([@Type]) ;
  • ligne 15 : [validate] ([class-validator]) vérifie toutes les contraintes. Il rend un tableau d'erreurs, vide si tout va bien. Chaque erreur indique le champ fautif ([property]) et les messages des contraintes violées ([constraints]) ;
  • lignes 17-18 : pas d'erreur : une page de bienvenue ;
  • lignes 20-23 : sinon, une page qui liste les erreurs. Dans les deux cas la réponse est une page HTML, avec le code 200.

Image

Image

C'est cette technique qu'utiliseront tous les formulaires de l'étude de cas (cf. exemple 19).

3.4. Exemple 10 : les paramètres multiples

Un même paramètre peut apparaître plusieurs fois dans une requête : c'est le cas des cases à cocher d'un formulaire, ou d'une liste à sélection multiple ([?couleur=rouge&couleur=vert]).

Le fichier [src/tableaux.controller.ts] :

import { Controller, Get, Query } from '@nestjs/common';
import { ChoixDto } from './choix.dto';

@Controller('tableaux')
export class TableauxController {
  // /tableaux/action01?couleur=rouge&couleur=vert : ce que reçoit Express
  @Get('action01')
  action01(@Query('couleur') couleur: string | string[]): object {
    return { couleur, estUnTableau: Array.isArray(couleur) };
  }

  // /tableaux/action02?couleur=rouge : toujours un tableau grâce à ChoixDto
  @Get('action02')
  action02(@Query() choix: ChoixDto): object {
    return { couleurs: choix.couleur, nombre: choix.couleur.length };
  }
}

L'action [action01] (lignes 7-10) montre ce que reçoit une action :

curl -i "http://localhost:3000/tableaux/action01?couleur=rouge"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 40

{"couleur":"rouge","estUnTableau":false}
curl -i "http://localhost:3000/tableaux/action01?couleur=rouge&couleur=vert"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 48

{"couleur":["rouge","vert"],"estUnTableau":true}

Le type dépend du nombre de valeurs : une chaîne pour une valeur, un tableau pour plusieurs. C'est un piège classique. La classe [ChoixDto] ramène les deux cas à un tableau :

Le fichier [src/choix.dto.ts] :

import { Transform } from 'class-transformer';
import { ArrayMinSize, IsArray, IsIn } from 'class-validator';

export class ChoixDto {
  // ?couleur=rouge -> "rouge" ; ?couleur=rouge&couleur=vert -> ["rouge","vert"]
  // @Transform ramène les deux cas à un tableau
  @Transform(({ value }) => (Array.isArray(value) ? value : [value]))
  @IsArray()
  @ArrayMinSize(1, { message: 'choisissez au moins une couleur' })
  @IsIn(['rouge', 'vert', 'bleu'], { each: true, message: 'couleur inconnue' })
  couleur: string[];
}
  • ligne 7 : [@Transform] ([class-transformer]) applique une fonction de conversion au champ : une valeur unique est placée dans un tableau ;
  • ligne 8 : le champ doit être un tableau...
  • ligne 9 : ... d'au moins un élément...
  • ligne 10 : ... et chaque élément ([each: true]) doit appartenir à la liste.
curl -i "http://localhost:3000/tableaux/action02?couleur=rouge"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 33

{"couleurs":["rouge"],"nombre":1}
curl -i "http://localhost:3000/tableaux/action02?couleur=rouge&couleur=vert"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 40

{"couleurs":["rouge","vert"],"nombre":2}
curl -i "http://localhost:3000/tableaux/action02?couleur=rouge&couleur=noir"
1
2
3
4
5
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 71

{"message":["couleur inconnue"],"error":"Bad Request","statusCode":400}

3.5. Exemple 11 : les paramètres d’une requête POST

Un formulaire HTML envoyé par la méthode POST place ses champs dans le corps de la requête, et non dans l'URL. Le décorateur [@Body] remplace alors [@Query] :

Le fichier [src/post.controller.ts] :

import { Body, Controller, Headers, Post } from '@nestjs/common';
import { PersonneDto } from './personne.dto';

@Controller('post')
export class PostController {
  // le corps de la requête, tel quel
  @Post('action01')
  action01(@Body() corps: Record<string, unknown>, @Headers('content-type') type: string): object {
    return { typeRecu: type, corps };
  }

  // un seul champ du corps
  @Post('action02')
  action02(@Body('nom') nom: string): string {
    return `nom = [${nom}]`;
  }

  // le corps, converti et validé par la classe PersonneDto
  @Post('action03')
  action03(@Body() personne: PersonneDto): object {
    return { personne, typeAge: typeof personne.age };
  }
}
  • lignes 7-10 : [@Body()] injecte le corps de la requête, déjà décodé par [NestJS]. [@Headers('content-type')] injecte un entête de la requête : le type du corps ;
  • lignes 13-16 : [@Body('nom')] injecte un seul champ du corps ;
  • lignes 19-22 : comme pour [@Query], le corps peut être décrit par une classe, validée par le [ValidationPipe] global.

L'option [-d] de [curl] envoie une requête POST avec un corps au format d'un formulaire HTML ([application/x-www-form-urlencoded]) :

curl -i -d "nom=martin&age=42" http://localhost:3000/post/action01
1
2
3
4
5
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 84

{"typeRecu":"application/x-www-form-urlencoded","corps":{"nom":"martin","age":"42"}}

Le même corps au format JSON (c'est ce qu'envoie un client [JavaScript]) :

curl -i -H "Content-Type: application/json" -d '{"nom":"martin","age":42}' http://localhost:3000/post/action01
1
2
3
4
5
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 65

{"typeRecu":"application/json","corps":{"nom":"martin","age":42}}

[NestJS] décode les deux formats. On remarque que l'âge est une chaîne dans le premier cas (un formulaire HTML ne transporte que du texte) et un nombre dans le second. Les deux autres actions :

curl -i -d "nom=martin&age=42" http://localhost:3000/post/action02
1
2
3
4
5
HTTP/1.1 201 Created
Content-Type: text/html; charset=utf-8
Content-Length: 14

nom = [martin]
curl -i -d "nom=martin&age=42&titre=Mme" http://localhost:3000/post/action03
1
2
3
4
5
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 71

{"personne":{"nom":"martin","age":42,"titre":"Mme"},"typeAge":"number"}
curl -i -d "nom=martin&age=200" http://localhost:3000/post/action03
1
2
3
4
5
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 99

{"message":["l'âge ne peut dépasser 150","titre inconnu"],"error":"Bad Request","statusCode":400}

Le code 201 (Created) est le code par défaut d'un POST pour [NestJS]. Pour une application HTML, une action POST répond soit par une page (on ajoutera alors [@HttpCode(200)]), soit par une redirection (cf. exemple 20).

3.6. Exemple 12 : pipes et décorateurs personnalisés

On peut écrire ses propres pipes et ses propres décorateurs de paramètres. L'étude de cas en fait un usage modéré, mais les connaître aide à comprendre ceux de [NestJS].

3.6.1. Un pipe : DateFrPipe

Le fichier [src/date-fr.pipe.ts] :

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

// transforme une date française "23/09/2026" en date ISO "2026-09-23"
@Injectable()
export class DateFrPipe implements PipeTransform<string, string> {
  transform(valeur: string): string {
    const m = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(valeur ?? '');
    if (!m) {
      throw new BadRequestException(`[${valeur}] n'est pas une date au format jj/mm/aaaa`);
    }
    const [, jour, mois, annee] = m;
    const iso = `${annee}-${mois}-${jour}`;
    // "31/02/2026" a la bonne forme, mais n'existe pas
    if (new Date(`${iso}T00:00:00Z`).toISOString().slice(0, 10) !== iso) {
      throw new BadRequestException(`la date [${valeur}] n'existe pas`);
    }
    return iso;
  }
}
  • lignes 4-5 : un pipe est une classe [@Injectable()] qui implémente l'interface [PipeTransform<entrée, sortie>] ;
  • ligne 6 : sa méthode [transform] reçoit la valeur du paramètre et rend la valeur transformée, que recevra l'action ;
  • lignes 7-9 : une valeur qui n'a pas la forme [jj/mm/aaaa] provoque une [BadRequestException] (400) ;
  • lignes 13-15 : une date qui a la bonne forme mais n'existe pas (31/02/2026) est refusée elle aussi : [new Date] la « corrige » en 03/03/2026, ce que l'on détecte.

3.6.2. Un décorateur de paramètre : @Langue()

Le fichier [src/langue.decorator.ts] :

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';

// @Langue() : la langue préférée du navigateur, lue dans l'entête
// "Accept-Language: fr-FR,fr;q=0.9,en;q=0.8" -> "fr"
export const Langue = createParamDecorator((defaut: string | undefined, ctx: ExecutionContext): string => {
  const req = ctx.switchToHttp().getRequest<Request>();
  const entete = req.get('accept-language') ?? '';
  const premiere = entete.split(',')[0].trim().slice(0, 2).toLowerCase();
  return premiere || defaut || 'fr';
});
  • ligne 6 : [createParamDecorator] fabrique un décorateur de paramètre, comme [@Query] ou [@Body]. La fonction reçoit l'argument du décorateur ([@Langue('fr')]) et le contexte d'exécution de la requête ;
  • ligne 7 : le contexte donne accès à la requête [Express] ;
  • lignes 8-10 : on rend les deux premières lettres de l'entête [Accept-Language], qui indique les langues préférées de l'utilisateur.

3.6.3. Le contrôleur

Le fichier [src/exemples.controller.ts] :

import { Controller, Get, Query } from '@nestjs/common';
import { DateFrPipe } from './date-fr.pipe';
import { Langue } from './langue.decorator';
import { RechercheDto } from './recherche.dto';

@Controller('exemples')
export class ExemplesController {
  // /exemples/agenda?jour=23/09/2026 : un pipe personnalisé
  @Get('agenda')
  agenda(@Query('jour', DateFrPipe) jour: string): string {
    return `agenda du jour [${jour}]`;
  }

  // /exemples/bonjour : un décorateur de paramètre personnalisé
  @Get('bonjour')
  bonjour(@Langue('fr') langue: string): string {
    return langue === 'en' ? 'Hello!' : 'Bonjour !';
  }

  // /exemples/recherche?motCle=nest&admin=true : [admin] n'est pas déclaré
  @Get('recherche')
  recherche(@Query() recherche: RechercheDto): object {
    return recherche;
  }
}
  • ligne 10 : le pipe s'utilise comme [ParseIntPipe] ;
  • ligne 16 : le décorateur s'utilise comme [@Query] ;
  • ligne 22 : un modèle décrit par une classe, avec cette fois l'option [forbidNonWhitelisted] du [ValidationPipe] (fichier [main.ts] ci-dessous).

Le fichier [src/main.ts] :

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      whitelist: true, // les champs non déclarés sont supprimés...
      forbidNonWhitelisted: true, // ... ou, avec cette option, refusés (400)
    }),
  );
  await app.listen(3000);
  console.log('Serveur démarré sur http://localhost:3000');
}

bootstrap();

ligne 11 : avec [forbidNonWhitelisted: true], un paramètre non déclaré dans la classe n'est plus supprimé silencieusement : il provoque une erreur 400.

Les résultats :

curl -i "http://localhost:3000/exemples/agenda?jour=23/09/2026"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 27

agenda du jour [2026-09-23]
curl -i "http://localhost:3000/exemples/agenda?jour=2026-09-23"
1
2
3
4
5
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 105

{"message":"[2026-09-23] n'est pas une date au format jj/mm/aaaa","error":"Bad Request","statusCode":400}
curl -i "http://localhost:3000/exemples/agenda?jour=31/02/2026"
1
2
3
4
5
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 86

{"message":"la date [31/02/2026] n'existe pas","error":"Bad Request","statusCode":400}
curl -i -H "Accept-Language: fr-FR,fr;q=0.9" http://localhost:3000/exemples/bonjour
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 9

Bonjour !
curl -i -H "Accept-Language: en-US,en;q=0.9" http://localhost:3000/exemples/bonjour
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 6

Hello!
curl -i "http://localhost:3000/exemples/recherche?motCle=nest"
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 17

{"motCle":"nest"}
curl -i "http://localhost:3000/exemples/recherche?motCle=nest&admin=true"
1
2
3
4
5
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 86

{"message":["property admin should not exist"],"error":"Bad Request","statusCode":400}

(l'option [-H] de [curl] ajoute un entête à la requête)

3.6.4. Conclusion

Le modèle d'une action est construit par [NestJS] à partir de la requête : paramètres de route ([@Param]), de l'URL ([@Query]), du corps ([@Body]), entêtes ([@Headers]). Les pipes convertissent et valident ces valeurs. Pour une application HTML, on préfère le plus souvent valider soi-même le modèle (exemple 09), afin de pouvoir réafficher le formulaire. C'est l'objet du chapitre suivant.