Passer au contenu

Pipes

Un pipe est une classe annotée avec le @Injectable() décorateur, qui implémente l’interface PipeTransform.

image

Les pipes ont deux cas d’utilisation typiques :

  • transformation : transformer les données d’entrée au format désiré (par exemple, de chaîne à entier)
  • validation : évaluer les données d’entrée et, si elles sont valides, les passer inchangées ; sinon, lever une exception.

Dans les deux cas, les pipes opèrent sur les arguments traités par un gestionnaire de route. Nest interpose un pipe juste avant qu’une méthode ne soit invoquée, et le pipe reçoit les arguments destinés à la méthode et agit dessus. Toute opération de transformation ou de validation a lieu à ce moment, après quoi le gestionnaire de route est invoqué avec des arguments (potentiellement) transformés.

Nest vient avec plusieurs pipes intégrés que vous pouvez utiliser directement. Vous pouvez également créer vos propres pipes personnalisés. Dans ce chapitre, nous allons introduire les pipes intégrés et montrer comment les lier aux gestionnaires de route. Nous examinerons ensuite plusieurs pipes construits sur mesure pour montrer comment vous pouvez en créer un à partir de zéro.

Pipes intégrés

Nest fournit neuf pipes disponibles directement :

  • ValidationPipe
  • ParseIntPipe
  • ParseFloatPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • ParseEnumPipe
  • DefaultValuePipe
  • ParseFilePipe

Ils sont exportés du package @nestjs/common.

Jetons un coup d’œil rapide à l’utilisation de ParseIntPipe. C’est un exemple du cas d’utilisation de transformation, où le pipe garantit qu’un paramètre de gestionnaire de méthode est converti en entier JavaScript (ou lève une exception si la conversion échoue). Plus loin dans ce chapitre, nous montrerons une simple implémentation personnalisée pour un ParseIntPipe. Les techniques d’exemple ci-dessous s’appliquent également aux autres pipes de transformation intégrés (ParseBoolPipe, ParseFloatPipe, ParseEnumPipe, ParseArrayPipe et ParseUUIDPipe, auxquels nous ferons référence comme les pipes Parse* dans ce chapitre).

Liaison des pipes

Pour utiliser un pipe, nous devons lier une instance de la classe pipe au contexte approprié. Dans notre exemple ParseIntPipe, nous voulons associer le pipe à une méthode de gestionnaire de route particulière et nous assurer qu’il s’exécute avant l’appel de la méthode. Nous procédons ainsi avec la construction suivante, que nous appellerons liaison du pipe au niveau des paramètres de méthode :

Exemple de liaison de pipe
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}

Cela garantit que l’une des deux conditions suivantes est vraie : soit le paramètre que nous recevons dans la méthode findOne() est un nombre (comme attendu dans notre appel à this.catsService.findOne()), soit une exception est levée avant que le gestionnaire de route ne soit appelé.

Par exemple, supposons que la route soit appelée comme suit :

Fenêtre de terminal
GET localhost:3000/abc

Nest lèvera une exception comme ceci :

{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}

L’exception empêchera l’exécution du corps de la méthode findOne().

Dans l’exemple ci-dessus, nous passons une classe (ParseIntPipe), pas une instance, laissant la responsabilité de l’instantiation au framework et permettant l’injection de dépendances. À l’instar des pipes et des gardes, nous pouvons également passer une instance en place. Passer une instance en place est utile si nous voulons personnaliser le comportement du pipe intégré en passant des options :

@Get(':id')
async findOne(@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE })) id: number) {
return this.catsService.findOne(id);
}

Liaison des autres pipes de transformation (tous les pipes de Parse fonctionnent de manière similaire. Ces pipes fonctionnent tous dans le contexte de la validation des paramètres de route, des paramètres de chaîne de requête et des valeurs du corps de requête.

Par exemple, avec un paramètre de chaîne de requête :

Exemple avec un paramètre de chaîne de requête
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}

Voici un exemple d’utilisation du ParseUUIDPipe pour analyser un paramètre de chaîne et valider qu’il s’agit d’un UUID.

Au-dessus, nous avons vu des exemples de liaison des divers pipes intégrés de la famille Parse*. La liaison des pipes de validation est un peu différente ; nous en discuterons dans la section suivante.

TypeScript interfêtes disparaissent lors de la transpilation. Ainsi, si le type d’un paramètre de méthode est déclaré en tant qu’interface au lieu d’une classe, la valeur metatype sera Object.

Validation basée sur un schéma

Raffinons notre pipe de validation. Prenons un moment pour examiner la méthode create() du CatsController, où nous aimerions probablement assurer que l’objet du corps de la publication est valide avant d’essayer d’exécuter notre méthode de service.

Méthode create dans CatsController
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}

Concentrons-nous sur le paramètre de corps createCatDto. Son type est CreateCatDto :

Classe CreateCatDto
export class CreateCatDto {
name: string;
age: number;
breed: string;
}

Nous voulons nous assurer que toute requête entrant dans la méthode de création contient un corps valide. Nous devons donc valider les trois membres de l’objet createCatDto. Nous pourrions le faire au sein de la méthode de gestionnaire de route, mais ce n’est pas idéal car cela rompt le principe de responsabilité unique (SRP).

Une autre approche pourrait être de créer une classe de validation et de déléguer la tâche là-bas. Cela a l’inconvénient que nous devrions nous souvenir d’appeler ce validateur au début de chaque méthode.

Comment créer un middleware de validation ? Cela pourrait fonctionner, mais malheureusement, il n’est pas possible de créer un middleware générique qui peut être utilisé dans tous les contextes de l’application entière. Cela est dû au fait que le middleware n’est pas conscient du contexte d’exécution, y compris le gestionnaire qui sera appelé et tous ses paramètres.

C’est, bien sûr, exactement le cas d’utilisation pour lequel les pipes sont conçus. Voyons donc comment nous pouvons affiner notre pipe de validation.

Validation d’objet basée sur un schéma

Il existe plusieurs approches pour effectuer une validation d’objet de manière propre, DRY. Une approche courante consiste à utiliser une validation basée sur un schéma. Allons-y et essayons cette approche.

La bibliothèque Zod vous permet de créer des schémas de manière simple, avec une API lisible. Construisons un pipe de validation qui utilise des schémas basés sur Zod.

Commencez par installer le package requis :

Fenêtre de terminal
$ npm install --save zod

Dans l’exemple de code ci-dessous, nous créons une classe simple qui prend un schéma comme argument de constructor. Nous appliquons ensuite la méthode schema.parse(), qui valide notre argument entrant par rapport au schéma fourni.

Comme mentionné précédemment, un pipe de validation renvoie soit la valeur inchangée, soit lève une exception.

Dans la section suivante, vous verrez comment nous fournissons le schéma approprié pour une méthode de contrôleur donnée en utilisant le décorateur @UsePipes(). Cela rend notre pipe de validation réutilisable à travers les contextes, tout comme nous l’avons prévu.

Exemple de pipe de validation avec Zod
import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) {}
transform(value: unknown, metadata: ArgumentMetadata) {
try {
const parsedValue = this.schema.parse(value);
return parsedValue;
} catch (error) {
throw new BadRequestException('Validation failed');
}
}
}

Nous avons donc vu nos différentes manières de créer des pipes de validation et d’effectuer des validations sur nos données dans NestJS. Cela montre comment la puissance des pipes peut vous aider à maintenir votre code propre, modulaire et efficace tout au long de votre application.