DEV Community

Amos Pascal
Amos Pascal

Posted on

Les intercepteurs en NestJS

NestJS est un framework Node.js qui permet de construire des applications évolutives et performantes en utilisant une approche basée sur les modules et l'injection de dépendances. L'un des aspects les plus puissants de NestJS est sa capacité à intercepter les requêtes HTTP entrantes et sortantes, ce qui permet de réaliser de nombreuses tâches, telles que l'authentification, la validation de données, la journalisation et bien plus encore.

Dans cet article, nous allons explorer les intercepteurs dans NestJS, en nous concentrant sur leur utilisation avancée pour résoudre des problèmes spécifiques. Nous allons également fournir des exemples de code pour vous aider à comprendre comment les intercepteurs fonctionnent et comment vous pouvez les utiliser dans vos propres projets.

Introduction aux intercepteurs

Un intercepteur est une classe qui implémente l'interface NestInterceptor. Cette interface définit une méthode intercept qui est appelée chaque fois qu'une requête HTTP est traitée par l'application. L'intercepteur peut alors effectuer des opérations avant ou après que la requête ne soit traitée par le contrôleur correspondant. Les intercepteurs peuvent également modifier la réponse envoyée au client ou la demande reçue par le serveur.

Voici un exemple simple d'un intercepteur qui affiche les demandes HTTP entrantes :

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log(`Incoming request to ${context.getArgs()[0].route.path}`);
    const now = Date.now();
    return next.handle().pipe(
      tap(() => console.log(`Request took ${Date.now() - now}ms`)),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Dans cet exemple, l'intercepteur LoggingInterceptor est injecté dans une application NestJS en tant que provider (@Injectable()). La méthode intercept est appelée pour chaque demande HTTP entrante, et elle affiche un message de journalisation pour suivre les requêtes reçues. La méthode next.handle()renvoie un observable qui permet d'effectuer des opérations asynchrones avant de poursuivre le traitement de la demande.

L'opérateur tap de RxJS est utilisé ici pour enregistrer le temps que prend la requête, puis imprimer ce temps dans la console. En fin de compte, l'observable est retourné pour permettre à la demande de continuer son traitement.

Utilisation avancée des intercepteurs

Les intercepteurs peuvent être utilisés pour résoudre un large éventail de problèmes. Voici quelques exemples d'utilisations avancées des intercepteurs dans NestJS :

  • Validation des données

Les intercepteurs peuvent être utilisés pour valider les données avant qu'elles ne soient traitées par le contrôleur. Voici un exemple d'intercepteur qui utilise la bibliothèque class-validator pour valider les données entrantes :

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';


@Injectable()
export class ValidationInterceptor implements NestInterceptor {
  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const dto = plainToClass(request.route.stack[0].methodOptions.dto, request.body);
    const errors = await validate(dto);
    if (errors.length > 0) {
      const errorResponse = { message: 'Validation failed', errors };
      throw new HttpException(errorResponse, HttpStatus.BAD_REQUEST);
    }
    return next.handle().pipe(map(data => data));
  }
}
Enter fullscreen mode Exit fullscreen mode

Dans cet exemple, l'intercepteur ValidationInterceptor utilise la méthode plainToClass de class-transformer pour transformer le corps de la demande en un objet de type DTO (Data Transfer Object), puis utilise la méthode validate de class-validator pour valider cet objet. Si des erreurs sont détectées, l'intercepteur lève une exception HTTP avec un message d'erreur. Sinon, la demande est autorisée à poursuivre son traitement.

  • Authentification

Les intercepteurs peuvent être utilisés pour authentifier les demandes avant qu'elles ne soient traitées par le contrôleur. Voici un exemple d'intercepteur qui vérifie si un utilisateur est authentifié avant d'autoriser une demande :

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptor implements NestInterceptor {
  constructor(private authService: AuthService) {}

  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];
    const user = await this.authService.validateToken(token);
    if (!user) {
      throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED);
    }
    request.user = user;
    return next.handle().pipe(map(data => data));
  }
}
Enter fullscreen mode Exit fullscreen mode

Dans cet exemple, l'intercepteur AuthInterceptor utilise un service d'authentification pour valider un token d'authentification présent dans l'en-tête de la demande. Si le token n'est pas valide ou n'est pas présent, l'intercepteur lève une exception HTTP avec le code d'état 401 Unauthorized. Si l'utilisateur est authentifié, l'objet utilisateur est ajouté à la demande avant de laisser la demande continuer son traitement.

  • Compression

Les intercepteurs peuvent être utilisés pour compresser les réponses envoyées par le serveur. Voici un exemple d'intercepteur qui utilise la bibliothèque compression pour compresser les réponses au format gzip :

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import * as compression from 'compression';

@Injectable()
export class CompressionInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const compressionMiddleware = compression();
    const res = context.switchToHttp().getResponse();
    compressionMiddleware(context.switchToHttp().getRequest(), res, () => {});
    return next.handle();
  }
}
Enter fullscreen mode Exit fullscreen mode

Dans cet exemple, l'intercepteur CompressionInterceptor utilise la bibliothèque compression pour compresser les réponses envoyées par le serveur. La méthode next.handle() est appelée pour permettre à la demande de continuer son traitement.

  • Téléchargement de fichiers

les intercepteurs peuvent également être utilisés pour gérer l'upload de fichiers dans NestJS, et permettre aux développeurs de changer de driver de stockage de fichiers sans avoir à modifier le code de l'application constamment.

Prenons l'exemple où nous avons besoin de stocker des fichiers téléchargés par les utilisateurs dans une application NestJS, et nous voulons que l'application soit capable de stocker les fichiers localement sur le disque, sur AWS S3 ou sur cloudinary en fonction des besoins. Nous pouvons créer un intercepteur qui agira en tant que middleware pour traiter les fichiers téléchargés, puis utiliser une interface commune pour stocker les fichiers sur différents services de stockage de fichiers.

Voici un exemple d'intercepteur qui peut gérer l'upload de fichiers dans NestJS en utilisant différentes options de stockage en fonction de la configuration de l'application :

import { Injectable, HttpException, HttpStatus, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { diskStorage, multer } from 'multer';
import { ConfigService } from '@nestjs/config';
import { S3 } from 'aws-sdk';
import { v2 } from 'cloudinary';
import * as multerS3 from 'multer-s3';
import { CloudinaryStorage } from 'multer-storage-cloudinary';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';


@Injectable()
export class FileInterceptor implements NestInterceptor {
  constructor(private readonly configService: ConfigService) { }

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.switchToHttp().getRequest();
    const driver = this.configService.get<string>('FILE_DRIVER');

    const storages = {
      local: () => diskStorage({
        destination: './files',
        filename: (request, file, callback) => {
          callback(
            null,
            `${randomStringGenerator()}.${file.originalname
              .split('.')
              .pop()
              .toLowerCase()}`,
          );
        },
      }),
      s3: () => {
        const s3 = new S3({
          accessKeyId: this.configService.get<string>('S3_ACCESS_KEY_ID'),
          secretAccessKey: this.configService.get<string>('S3_SECRET_ACCESS_KEY'),
          region: this.configService.get<string>('S3_REGION'),
        });

        return multerS3({
          s3: s3,
          bucket: this.configService.get<string>('S3_BUCKET'),
          acl: 'public-read',
          contentType: multerS3.AUTO_CONTENT_TYPE,
          key: (request, file, callback) => {
            callback(
              null,
              `${randomStringGenerator()}.${file.originalname
                .split('.')
                .pop()
                .toLowerCase()}`,
            );
          },
        });
      },
      cloudinary: () => {
        v2.config({
          cloud_name: this.configService.get<string>('CLOUDINARY_CLOUD_NAME'),
          api_key: this.configService.get<string>('CLOUDINARY_API_KEY'),
          api_secret: this.configService.get<string>('CLOUDINARY_API_SECRET'),
        });

        return new CloudinaryStorage({
          cloudinary: v2,
          params: () => {
            return {
              folder: '',
              format: 'jpeg',
              allowedFormats: ['jpg', 'png', 'pdf'],
              unique_filename: true,
              resource_type: 'auto',
              use_filename: false,
            };
          },
        });
      }
    }

    const multerOptions: MulterOptions = {
      fileFilter: (request, file, callback) => {
        if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/i)) {
          return callback(
            new HttpException(
              {
                status: HttpStatus.UNPROCESSABLE_ENTITY,
                errors: {
                  file: `cantUploadFileType`,
                },
              },
              HttpStatus.UNPROCESSABLE_ENTITY,
            ),
            false
          );
        }

        callback(null, true);
      },
      storage: storages[driver](),
      limits: {
        fileSize: this.configService.get<string>('FILE_SIZE'),
      },
    };

    const upload = multer(multerOptions).single('file');
    return new Observable((observer) => {
      upload(req, undefined, (err) => {
        if (err) {
          observer.error(err);
        } else {
          observer.next(req.file);
          observer.complete();
        }
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Dans cet exemple, nous avons créé un intercepteur appelé FileInterceptor qui gère l'upload de fichiers. Dans la méthode intercept, nous avons récupéré la requête HTTP en utilisant switchToHttp().getRequest(), nous avons également créé une variable driver qui contient la configuration du service de stockage de fichiers que nous voulons utiliser puis nous avons configuré multer (qui est un middleware populaire pour le traitement des fichiers dans Node.js) en fonction du driver dans un objet littéral appelé storages.

En fonction du driver, nous avons configuré multer pour stocker les fichiers localement sur le disque, sur AWS S3 ou sur cloudinary.

Nous avons utilisé la bibliothèque aws-sdk pour interagir avec AWS S3, et la bibliothèque cloudinary pour interagir avec le service Cloudinary.

Une fois que nous avons configuré multer, nous avons créé un upload middleware en utilisant multer(multerOptions).single('file'), qui peut traiter les fichiers téléchargés et les stocker dans le service de stockage approprié.

En utilisant cette approche, nous pouvons changer de driver de stockage de fichiers simplement en modifiant la configuration de l'application, sans avoir à modifier le code de l'application. Par exemple, si nous voulons utiliser AWS S3 à la place de cloudinary, nous pouvons modifier la configuration de l'application en conséquence, et notre application commencera à stocker les fichiers sur AWS S3.

Conclusion

Les intercepteurs sont un outil puissant dans NestJS qui permet de résoudre une grande variété de problèmes. Ils peuvent être utilisés pour valider les données, authentifier les demandes, compresser les réponses, faciliter la gestion des téléchargements et bien plus encore. Dans cet article, nous avons fourni des exemples de code pour vous aider à comprendre comment les intercepteurs fonctionnent et comment vous pouvez les utiliser dans vos propres projets. J'espére que cet article vous a été utile et que vous pourrez l'utiliser pour améliorer vos applications, Boilerplate NestJS...

Top comments (0)