Fermer

octobre 15, 2025

Comment diffuser des fichiers volumineux avec des flux Node.js dans NestJS

Comment diffuser des fichiers volumineux avec des flux Node.js dans NestJS


Apprenez à gérer des fichiers volumineux de manière efficace et fiable sur un serveur NestJS avec les flux Node.js, les compartiments S3 et CSV vers JSON.

Dans cet article, nous allons apprendre à gérer des fichiers volumineux sur un Pas serveur efficacement. En utilisant Flux Node.jsnous nous occuperons des téléchargements, des téléchargements sur le disque et Compartiments S3 et même le traitement de fichiers avec un exemple CSV vers JSON. Après avoir lu cet article, vous n’aurez plus à vous soucier du crash de votre serveur à cause de fichiers volumineux.

Conditions préalables

Pour suivre et tirer le meilleur parti de cet article, vous aurez besoin de connaissances de base sur le fonctionnement général des téléchargements HTTP et d’une certaine familiarité avec Papillon de nuit pour la gestion des téléchargements, une connaissance de base du SDK AWS S3 et une compréhension de base de l’architecture NestJS.

Configuration du projet

Commençons par créer un projet NestJS :

nest new stream-app
cd stream-app

Ensuite, exécutez la commande ci-dessous pour créer le FilesModule, FilesController, FilesService et CSVController fichiers :

nest g module files \
&& nest g controller files \
&& nest g service files \
&& nest g controller files/csv \
&& nest g service files/csv \
&& nest g controller files/s3 \
&& nest g service files/s3

Installez les dépendances dont nous aurons besoin pour ce projet :

npm install multer csv-parser mime-types @aws-sdk/client-s3 @nestjs/config
npm install -D @types/multer @types/mime-types

Ici, nous utiliserons multer pour gérer les téléchargements, csv-parser pour transformer CSV en JSON, mime-types pour définir le type de contenu correct pour les fichiers, @aws-sdk/client-s3 pour télécharger des fichiers sur un service de stockage compatible S3 (DigitalOcean Spaces) et @nestjs/config pour récupérer les variables d’environnement.

Ensuite, pour utiliser le service NestJS Config dans notre application, nous devons importer le ConfigModule. Mettez à jour votre app.module.ts fichier avec les éléments suivants :

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { FilesModule } from "./files/files.module";
import { ConfigModule } from "@nestjs/config";

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true }), FilesModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Enfin, dans le répertoire racine, créez un dossier appelé storage et ajoutez-y un fichier volumineux (au moins 100 Mo pour montrer les avantages du streaming en termes de mémoire).

Par exemple:

stream - app / storage / large - report.pdf;

Streaming de base dans NestJS

Maintenant, la mauvaise façon d’envoyer un fichier volumineux à un utilisateur est d’utiliser readFileSync(). Cela charge l’intégralité du fichier en mémoire et l’envoie en une seule fois. Cette approche n’est pas pratique pour les fichiers volumineux ou les applications à grande échelle.


@Get('download-bad')
getFileBad(@Res() res: Response) {
  const filePath = join(process.cwd(), 'storage', 'large-report.pdf');
  const fileBuffer = readFileSync(filePath); 

  res.setHeader('Content-Type', 'application/pdf');
  res.setHeader('Content-Disposition', 'attachment; filename="report.pdf"');

  return res.send(fileBuffer); 
}

Heureusement, Node.js nous permet de travailler avec des flux. Les flux sont un moyen de gérer les données de manière efficace, progressive et non bloquante. De cette façon, les données sont traitées par morceaux plutôt que dans leur ensemble. En utilisant createReadStream()nous lisons un fichier par morceaux de 64 Ko (par défaut).

Mettez à jour votre files.controller.ts fichier avec les éléments suivants :

import {
  Controller,
  Get,
  Query,
  Res,
  HttpException,
  HttpStatus,
  Post,
  UploadedFile,
  UseInterceptors,
  ConsoleLogger,
} from "@nestjs/common";
import { Response } from "express";
import { extname, join } from "path";
import { createReadStream, statSync } from "fs";
import { StreamableFile } from "@nestjs/common";
import * as mime from "mime-types";
import { FilesService } from "./files.service";
import { FileInterceptor } from "@nestjs/platform-express";
import { diskStorage } from "multer";

@Controller("files")
export class FilesController {
  constructor(private readonly filesService: FilesService) {}

  @Get("download")
  getFile(@Res({ passthrough: true }) res: Response) {
    const filePath = join(process.cwd(), "storage", "large-report.pdf");
    const fileStream = createReadStream(filePath);

    res.set({
      "Content-Type": "application/pdf",
      "Content-Disposition": 'attachment; filename="report.pdf"',
    });

    return new StreamableFile(fileStream);
  }
}

Dans le code ci-dessus, le @Res({ passthrough: true }) decorator dit à NestJS de nous permettre de définir les en-têtes (modifier la réponse) tout en gérant le renvoi des données de réponse (ce qui signifie que nous n’avons pas besoin d’appeler res.send()).

Les en-têtes que nous définissons sont :

  • Content-Type: Qui indique au navigateur le type de fichier que nous envoyons
  • Content-Disposition: Qui indique au navigateur comment nommer le fichier et télécharger le fichier

StreamableFile(fileStream) encapsule le flux brut, permettant à NestJS de comprendre comment le renvoyer en réponse. Cette approche fonctionne à la fois pour Express et Fastify. Le StreamableFile classe gère les différences de bas niveau, donc si vous souhaitez passer à Fastify, il vous suffit de modifier votre main.ts fichier et installez l’adaptateur.

Téléchargement de fichiers amélioré

L’exemple précédent fonctionne, mais en production, nous souhaiterons plus de choses comme une meilleure gestion des erreurs, une validation des entrées, des en-têtes correctement définis et une logique réutilisable.

Mettez à jour votre files.service.ts fichier avec les éléments suivants :

import {
  Injectable,
  StreamableFile,
  NotFoundException,
  BadRequestException,
} from "@nestjs/common";
import { join } from "path";
import { createReadStream, existsSync } from "fs";
import { ReadStream } from "fs";

@Injectable()
export class FilesService {
  getFileStream(fileName: string): { stream: ReadStream; path: string } {
    try {
      
      if (!fileName || typeof fileName !== "string") {
        throw new BadRequestException("Invalid filename provided");
      }

      
      if (
        fileName.includes("..") ||
        fileName.includes("https://www.telerik.com/") ||
        fileName.includes("\\")
      ) {
        throw new BadRequestException(
          "Invalid filename: contains path traversal characters"
        );
      }

      const filePath = join(process.cwd(), "storage", fileName);

      if (!existsSync(filePath)) {
        throw new NotFoundException(`File '${fileName}' not found`);
      }

      const stream = createReadStream(filePath);
      return { stream, path: filePath };
    } catch (error) {
      if (
        error instanceof NotFoundException ||
        error instanceof BadRequestException
      ) {
        throw error;
      }
      throw new BadRequestException(
        `Failed to get file stream for ${fileName}: ${error.message}`
      );
    }
  }
}

Dans le code ci-dessus, nous effectuons d’abord une validation de base du nom de fichier pour empêcher les valeurs nulles ou non définies de provoquer des plantages. Ensuite, nous améliorons la sécurité en empêchant les attaques par traversée de répertoire (nous bloquons les tentatives d’accès aux fichiers en dehors du répertoire de stockage). Enfin, nous implémentons une gestion appropriée des erreurs à l’aide des exceptions de NestJS.

Veuillez noter que existsSync() vérifie si un chemin ou un répertoire spécifié existe ; il renvoie vrai si c’est le cas et faux si ce n’est pas le cas.

Maintenant, mettez à jour votre *files.controller.ts* pour inclure le point de terminaison suivant :

@Get('improved-download')
downloadFile(@Query('name') name: string, @Res({ passthrough: true }) res: Response) {
  if (!name) {
    throw new HttpException('Filename is required', HttpStatus.BAD_REQUEST);
  }

  const { stream, path } = this.filesService.getFileStream(name);
  const fileSize = statSync(path).size;
  const fileExtension = extname(path);
  const contentType = mime.lookup(fileExtension) || 'application/octet-stream';

  res.set({
    'Content-Type': contentType,
    'Content-Disposition': `attachment; filename="${name}"`,
    'Content-Length': fileSize.toString(),
    'Cache-Control': 'no-cache, no-store, must-revalidate',
  });

  return new StreamableFile(stream);
}

Dans le code ci-dessus, nous implémentons d’abord la sélection dynamique de fichiers à l’aide d’un paramètre de requête name. Nous transmettons ensuite ce nom à notre getFileStream(name) méthode pour obtenir le flux et le chemin. En utilisant statSync()nous obtenons la taille du fichier, que nous définirons dans les en-têtes pour aider les navigateurs à afficher des barres de progression pendant le téléchargement. Nous obtenons l’extension du fichier et utilisons la bibliothèque de types MIME pour la mapper au type MIME précis, par exemple : application/pdf ou image/jpeg. Enfin, nous définissons les en-têtes avant de permettre à NestJS de gérer la réponse.

Lorsque nous téléchargeons des fichiers, les navigateurs mettent parfois en cache la réponse, ce qui peut entraîner des problèmes tels que l’obtention de versions obsolètes des fichiers par les utilisateurs. En définissant Cache-Controlnous évitons de tels problèmes.

Téléchargement de fichiers volumineux

Voyons ensuite comment gérer les téléchargements avec des flux. Nous passerons en revue les téléchargements en streaming sur le disque et les téléchargements en streaming vers les compartiments S3.

Téléchargement sur le disque

Ajoutez le Post/upload itinéraire ci-dessous vers le FilesController :

@Post('upload')
  @UseInterceptors(
    FileInterceptor('file', {
      storage: diskStorage({
        destination: './uploads',
        filename: (req, file, callback) => {
          const uniqueName = Date.now() + extname(file.originalname);
          callback(null, uniqueName);
        },
      }),
      limits: {
        fileSize: 500 * 1024 * 1024, 
      },
    }),
  )
handleUpload(@UploadedFile() file: Express.Multer.File) {
  return {
    message: 'File uploaded successfully',
    filename: file.filename,
    size: file.size,
  };
}

Dans le code ci-dessus, @UseInterceptors est un décorateur NestJS qui nous permet d’attacher des intercepteurs à un gestionnaire de route. Nous attachons ici FileInterceptorqui est un assistant NestJS qui encapsule Multer, nous aide à récupérer le fichier de la requête, à l’analyser avec Multer et à le rendre disponible dans notre contrôleur avec le @UploadedFile() décorateur.

FileInterceptor prend le nom du champ dans les données du formulaire qui contient notre fichier (file) et l’objet de configuration Multer. Nous définissons storage à diskStorage au lieu de mettre le fichier en mémoire tampon. De cette façon, nous écrivons le fichier morceau par morceau au fur et à mesure de sa réception.

Le diskStorage() la méthode prend le destination (le répertoire dans lequel nous voulons enregistrer le fichier) et filenamequi est une fonction utilisée pour déterminer le nom du fichier.

Enfin, avec le @UploadedFile() décorateur, nous avons accès à l’objet fichier, qui contient des informations telles que le filename, originalname, mimetype, size, path et buffer. Mais comme nous définissons le stockage sur diskStorage, file.buffer serait undefined. En utilisant le file objet, nous renvoyons quelques détails en réponse pour montrer que le téléchargement a réussi.

Téléchargement vers S3

Ici, nous allons d’abord télécharger le fichier en utilisant diskStorage()puis diffusez-le directement dans notre compartiment S3.

Dans cet exemple, nous utiliserons DigitalOcean Spaces, qui est entièrement compatible S3. Il utilise le SDK AWS de la même manière, mais avec un point de terminaison personnalisé et une URL CDN de DigitalOcean.

Mettez à jour votre s3.service.ts fichier avec les éléments suivants :

import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { Readable } from "stream";
import * as path from "path";

@Injectable()
export class S3Service {
  private s3: S3Client;
  private readonly bucketName: string;
  private readonly endpoint: string;
  private readonly region: string;
  private readonly cdnUrl: string;

  constructor(private readonly configService: ConfigService) {
    this.bucketName = this.configService.getOrThrow<string>(
      "DIGITAL_OCEAN_SPACE_BUCKET_NAME"
    );
    this.endpoint = this.configService.getOrThrow<string>(
      "DIGITAL_OCEAN_SPACE_ENDPOINT"
    );
    this.region = this.configService.getOrThrow<string>(
      "DIGITAL_OCEAN_SPACE_REGION"
    );
    this.cdnUrl = this.configService.getOrThrow<string>(
      "DIGITAL_OCEAN_SPACE_CDN_URL"
    );
    const accessKeyId = this.configService.getOrThrow<string>(
      "DIGITAL_OCEAN_SPACE_ACCESS_KEY_ID"
    );
    const secretAccessKey = this.configService.getOrThrow<string>(
      "DIGITAL_OCEAN_SPACE_SECRET_KEY"
    );
    this.s3 = new S3Client({
      endpoint: this.endpoint,
      forcePathStyle: false,
      region: this.region,
      credentials: {
        accessKeyId,
        secretAccessKey,
      },
    });
  }

  async uploadImageStream(payload: {
    location: string;
    file: {
      stream: Readable;
      filename: string;
      mimetype: string;
      size: number;
    };
  }): Promise<{ path: string; key: string }> {
    const { location, file } = payload;
    const uid = Date.now().toString(); 
    const extension = path.extname(file.filename);
    const key = `${location}/${uid}${extension}`;

    const command = new PutObjectCommand({
      Bucket: this.bucketName,
      Key: key,
      Body: file.stream,
      ContentLength: file.size,
    });

    try {
      await this.s3.send(command);
      return {
        path: `${this.cdnUrl}/${key}`,
        key,
      };
    } catch (error) {
      console.error("Error uploading file stream:", error);
      throw new Error("File upload failed");
    }
  }
}

Dans notre uploadImageStream() méthode, nous définissons d’abord une clé unique pour le fichier ou l’objet, puis nous configurons la commande de téléchargement AWS SDK v3, en transmettant le flux lisible comme corps et en définissant le ContentLength.

Enfin, nous effectuons le téléchargement dans notre try-catch bloquer et renvoyer le path et key.

Ensuite, mettez à jour votre s3.controller.ts déposer avec ce qui suit ;

import {
  Controller,
  Post,
  UploadedFile,
  UseInterceptors,
  BadRequestException,
} from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";
import { diskStorage } from "multer";
import * as fs from "fs";
import * as path from "path";
import { S3Service } from "./s3.service";

@Controller("s3")
export class S3Controller {
  constructor(private readonly s3Service: S3Service) {}

  @Post("upload")
  @UseInterceptors(
    FileInterceptor("file", {
      storage: diskStorage({
        destination: "./uploads",
        filename: (req, file, cb) => {
          cb(null, `${Date.now()}-${file.originalname}`);
        },
      }),
      limits: { fileSize: 200 * 1024 * 1024 },
    })
  )
  async uploadToS3(@UploadedFile() file: Express.Multer.File) {
    if (!file) {
      throw new BadRequestException("No file uploaded");
    }

    const location = "uploads";
    const filePath = file.path; 
    const readStream = fs.createReadStream(filePath);
    const { size } = fs.statSync(filePath);

    try {
      const uploadResult = await this.s3Service.uploadImageStream({
        location,
        file: {
          stream: readStream,
          filename: file.originalname,
          mimetype: file.mimetype,
          size,
        },
      });

      return {
        message: "File uploaded to S3",
        ...uploadResult,
      };
    } catch (error) {
      throw new Error(`File upload failed: ${error.message}`);
    } finally {
      
      if (file.path && fs.existsSync(file.path)) {
        fs.unlinkSync(file.path);
      }
    }
  }
}

Dans notre uploadToS3 gestionnaire de route, nous passons le location et file au uploadImageStream() méthode et renvoie une réponse de succès avec le key et path. Si une erreur se produit, nous la renvoyons. Enfin, nous effaçons le fichier temporairement stocké de notre disque en utilisant fs.unlinkSync(file.path).

Traitement de fichiers volumineux : exemple CSV-JSON

Mettez à jour votre csv.service.ts fichier avec les éléments suivants :

import { Injectable, BadRequestException } from "@nestjs/common";
import * as csv from "csv-parser";
import { Readable } from "stream";

export interface CsvRow {
  [key: string]: string;
}

export interface CsvProcessingResult {
  totalRows: number;
  data: CsvRow[];
}

@Injectable()
export class CsvService {
  async processCsvStream(fileStream: Readable): Promise<CsvProcessingResult> {
    return new Promise((resolve, reject) => {
      const results: CsvRow[] = [];
      

      
      const csvStream = csv();

      
      csvStream.on("error", (error) => {
        reject(new BadRequestException(`CSV parsing failed: ${error.message}`));
      });

      
      csvStream.on("end", () => {
        resolve({
          totalRows: results.length,
          data: results,
        });
      });

      
      fileStream.pipe(csvStream).on("data", (data: CsvRow) => {
        results.push(data);
        
        
        
      });
    });
  }
}

Dans le processCsvStream() méthode, nous créons d’abord une nouvelle promesse pour gérer la nature asynchrone du streaming. Le tableau résultant est l’endroit où chaque ligne analysée du CSV sera stockée au fur et à mesure de son arrivée (pour les fichiers plus volumineux, remplacez-la par la logique de la base de données). Ensuite, nous créons un flux CSV en utilisant csv() depuis csv-parserqui fonctionne comme un flux de transformation. Un flux de transformation est un flux capable de lire et d’écrire des données (nous lisons des données CSV brutes et les écrivons en JSON, une ligne à la fois).

fileStream.pipe(csvStream) envoie des morceaux de données CSV brutes dans le csv-parser. Lorsque les données sont dans le csv-parseril le convertit en JSON ligne par ligne. Une fois le csv-parser a converti une ligne en objet JSON, il émet cet objet JSON en tant qu’événement de données. Cet événement de données est ensuite géré par notre gestionnaire d’événements, qui prend chaque objet JSON résultant et le pousse dans notre results tableau.

Nous avons deux autres gestionnaires d’événements pour error et end. Quand un error l’événement est reçu, nous rejetons la promesse avec une exception de mauvaise demande. Quand le end L’événement est reçu, cela signifie que l’intégralité du fichier CSV a été traité, et nous résolvons ensuite la promesse avec les résultats collectés.

Maintenant, mettez à jour votre csv.controller.ts fichier avec les éléments suivants :

import {
  Controller,
  Post,
  UploadedFile,
  UseInterceptors,
  BadRequestException,
} from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";
import { diskStorage } from "multer";
import * as fs from "fs";
import { CsvService } from "./csv.service";

@Controller("csv")
export class CsvController {
  constructor(private readonly csvService: CsvService) {}

  @Post("upload")
  @UseInterceptors(
    FileInterceptor("file", {
      storage: diskStorage({
        destination: "./uploads",
        filename: (req, file, cb) => {
          cb(null, `${Date.now()}-${file.originalname}`);
        },
      }),
      limits: { fileSize: 50 * 1024 * 1024 }, 
    })
  )
  async handleCsvUpload(@UploadedFile() file: Express.Multer.File) {
    if (!file) {
      throw new BadRequestException("No file uploaded");
    }

    
    const fileStream = fs.createReadStream(file.path);

    try {
      
      const result = await this.csvService.processCsvStream(fileStream);

      return {
        message: "CSV processed successfully",
        filename: file.originalname,
        ...result,
      };
    } catch (error) {
      throw new BadRequestException(`CSV processing failed: ${error.message}`);
    } finally {
      
      if (file.path && fs.existsSync(file.path)) {
        fs.unlinkSync(file.path);
      }
    }
  }
}

Enfin, vérifiez votre files.module.ts et vérifiez que tous les fournisseurs et contrôleurs sont correctement configurés, comme indiqué ci-dessous :

import { Module } from "@nestjs/common";
import { FilesController } from "./files.controller";
import { FilesService } from "./files.service";
import { CsvController } from "./csv/csv.controller";
import { S3Controller } from "./s3/s3.controller";
import { S3Service } from "./s3/s3.service";
import { CsvService } from "./csv/csv.service";

@Module({
  controllers: [FilesController, CsvController, S3Controller],
  providers: [FilesService, S3Service, CsvService],
})
export class FilesModule {}

Conclusion

Dans cet article, nous avons expliqué comment gérer les téléchargements de fichiers, les téléchargements sur le disque et S3 ainsi que le traitement des fichiers. Vous savez désormais quoi faire, quoi ne pas faire et les raisons. Les prochaines étapes possibles consistent à ajouter une logique de base de données à l’exemple CSV vers JSON ou à ajouter une logique de nouvelle tentative pour les téléchargements S3.




Source link