Les intercepteurs en NestJS
Tags: javascript, typescript, nestjs, node
Published: 2023-02-28 08:23:55

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 :

```typescript
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`)),
    );
  }
}
```

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 :

```typescript
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));
  }
}
```

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 :

```typescript
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));
  }
}
```

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 :

```typescript
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();
  }
}
```
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 :

```typescript
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();
        }
      });
    });
  }
}
```

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...