Diffuser de grandes réponses avec StreamableFile
Servez efficacement les téléchargements volumineux avec StreamableFile et les flux lisibles de Node afin d’éviter la mise en mémoire tampon.
Diffuser de grandes réponses avec StreamableFile est une leçon NestJS Enterprise Backend APIs gratuite sur CoddyKit. Ceci est la leçon 2 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage NestJS Enterprise Backend APIs, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours NestJS Enterprise Backend APIs comprend 4 leçons au total.
Certaines parties de cette leçon n'ont pas encore été traduites et s'affichent en anglais.
The Buffering Problem
When a controller returns a large file, the naive approach is to read the whole file into memory and send it back:
fs.readFileSync('huge.zip')loads every byte into RAM before the first byte reaches the client.- A 2 GB export served to 50 concurrent users can exhaust heap and crash the process.
Streaming solves this: read the file in small chunks and pipe them to the response as they arrive, keeping memory usage flat regardless of file size.
What StreamableFile Is
NestJS ships a StreamableFile class. You wrap a Node.js Readable stream (or a Buffer) in it and return it from a controller handler.
- Nest detects the
StreamableFilereturn value and pipes the underlying stream to the HTTP response for you. - It works across both Express and Fastify adapters without you touching
res.pipe()manually.
This keeps your handler declarative while still streaming chunk-by-chunk.
import { Controller, Get, StreamableFile } from '@nestjs/common';
import { createReadStream } from 'fs';
import { join } from 'path';
@Controller('files')
export class FilesController {
@Get('report')
getReport(): StreamableFile {
const file = createReadStream(join(process.cwd(), 'report.pdf'));
return new StreamableFile(file);
}
}How a Readable Stream Flows
Under the hood, createReadStream emits 'data' events with small Buffer chunks (64 KB by default). The response consumes them one at a time.
Here is a plain Node demonstration of chunked reading without any framework — notice memory never holds the whole file at once:
import { Readable } from 'stream';
// Simulate a large source as a stream of chunks
async function* generateChunks() {
for (let i = 0; i < 5; i++) {
yield `chunk-${i} `;
}
}
const stream = Readable.from(generateChunks());
stream.on('data', (chunk: Buffer | string) => {
console.log('received:', chunk.toString().trim());
});
stream.on('end', () => console.log('done streaming'));Setting Content-Type and Filename
By default the browser does not know what the stream is. Pass options to StreamableFile so Nest sets the right headers:
typesets theContent-Typeheader.dispositionsetsContent-Dispositionso the browser downloads with a filename instead of rendering inline.
import { Controller, Get, StreamableFile } from '@nestjs/common';
import { createReadStream } from 'fs';
import { join } from 'path';
@Controller('files')
export class FilesController {
@Get('invoice')
getInvoice(): StreamableFile {
const file = createReadStream(join(process.cwd(), 'invoice.pdf'));
return new StreamableFile(file, {
type: 'application/pdf',
disposition: 'attachment; filename="invoice.pdf"',
});
}
}Headers via @Header vs StreamableFile options
You can also set headers with the @Header() decorator, but mixing both can conflict. Prefer the StreamableFile options because Nest applies them consistently on both adapters.
- Use
@Header('Content-Type', ...)only when the value is static and known at compile time. - Use the
StreamableFiletype/dispositionoptions when the value is computed per request (e.g. a dynamic filename).
import { Controller, Get, Header, StreamableFile } from '@nestjs/common';
import { createReadStream } from 'fs';
@Controller('exports')
export class ExportsController {
@Get('static')
@Header('Content-Type', 'text/csv')
@Header('Content-Disposition', 'attachment; filename="data.csv"')
download(): StreamableFile {
return new StreamableFile(createReadStream('data.csv'));
}
}Streaming Generated Content (no file on disk)
StreamableFile is not limited to disk files. Any Readable works — including data you generate on the fly. This is ideal for large CSV exports built row-by-row from a database cursor.
Below, a generator produces CSV lines lazily so the whole dataset is never materialized in memory at once.
import { Controller, Get, StreamableFile, Header } from '@nestjs/common';
import { Readable } from 'stream';
@Controller('exports')
export class CsvExportController {
@Get('users.csv')
@Header('Content-Type', 'text/csv')
exportUsers(): StreamableFile {
async function* rows() {
yield 'id,name\n';
for (let i = 1; i <= 100000; i++) {
yield `${i},user_${i}\n`;
}
}
return new StreamableFile(Readable.from(rows()));
}
}Backpressure: Why Streaming Stays Memory-Safe
Backpressure is the mechanism that keeps streaming safe. When the client (or network) is slow, the writable side signals the readable side to pause producing chunks.
- A fast disk read paired with a slow client will not pile up gigabytes in memory.
- Node's
pipe()(used internally by Nest) handles this automatically — pausing and resuming the source.
This is exactly why you should return a stream instead of a giant Buffer.
import { Writable, Readable } from 'stream';
const source = Readable.from(['a', 'b', 'c', 'd', 'e']);
const slowSink = new Writable({
write(chunk, _enc, cb) {
console.log('wrote:', chunk.toString());
setTimeout(cb, 10); // simulate slow consumer -> triggers backpressure
},
});
source.pipe(slowSink);
slowSink.on('finish', () => console.log('all chunks flushed safely'));Handling Stream Errors
If the underlying stream errors after headers are sent, you cannot send a JSON error body anymore. StreamableFile exposes an error handler so you can log and close cleanly.
- Use
getStream().on('error', ...)or thesetErrorHandler()/error-handling option to react to read failures. - Without this, a missing file can leave the connection hanging or crash the request.
import { Controller, Get, StreamableFile, Logger } from '@nestjs/common';
import { createReadStream } from 'fs';
@Controller('files')
export class SafeFilesController {
private readonly logger = new Logger(SafeFilesController.name);
@Get('archive')
getArchive(): StreamableFile {
const stream = createReadStream('archive.zip');
const file = new StreamableFile(stream);
file.setErrorHandler((err, response) => {
this.logger.error(`Stream failed: ${err.message}`);
response.statusCode = 404;
response.end('File not available');
});
return file;
}
}Streaming from Object Storage (S3)
In enterprise apps the file usually lives in S3 or another object store, not local disk. The S3 SDK returns a readable stream for the object body — pass it straight to StreamableFile.
- No temp file, no full download into the API server's memory.
- Bytes flow S3 → your API → client as a relay.
import { Controller, Get, Param, StreamableFile } from '@nestjs/common';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { Readable } from 'stream';
@Controller('media')
export class MediaController {
private s3 = new S3Client({ region: 'eu-central-1' });
@Get(':key')
async download(@Param('key') key: string): Promise<StreamableFile> {
const obj = await this.s3.send(
new GetObjectCommand({ Bucket: 'my-bucket', Key: key }),
);
return new StreamableFile(obj.Body as Readable);
}
}Accessing the Raw Response with @Res({ passthrough })
Sometimes you need the raw response object to set status codes or extra headers while still letting Nest pipe the StreamableFile. Use @Res({ passthrough: true }) so Nest keeps control of the response lifecycle.
- Without
passthrough: true, injecting@Res()makes you responsible for ending the response, and returning a StreamableFile no longer works automatically.
import { Controller, Get, Res, StreamableFile } from '@nestjs/common';
import type { Response } from 'express';
import { createReadStream, statSync } from 'fs';
@Controller('files')
export class RangeController {
@Get('video')
getVideo(@Res({ passthrough: true }) res: Response): StreamableFile {
const { size } = statSync('movie.mp4');
res.set({ 'Content-Length': size, 'Accept-Ranges': 'bytes' });
return new StreamableFile(createReadStream('movie.mp4'));
}
}Transforming a Stream on the Fly
You can chain transforms before handing the stream to StreamableFile. A common case is gzip-compressing a large export so less data crosses the wire — still chunk-by-chunk.
Here is a standalone demo of piping through a transform without any server:
import { Readable, Transform } from 'stream';
const upper = new Transform({
transform(chunk, _enc, cb) {
cb(null, chunk.toString().toUpperCase());
},
});
const source = Readable.from(['hello ', 'streamed ', 'world']);
source.pipe(upper).on('data', (c) => console.log(c.toString()));
upper.on('end', () => console.log('transform complete'));Quick Check
Test your understanding of the core decision behind StreamableFile.
Recap
Key takeaways for streaming large responses in NestJS:
- Return
new StreamableFile(readable)instead of buffering whole files into memory. - Wrap any
Readable: acreateReadStream, an S3 object body, or a generator-backed stream. - Set
typeanddisposition(or@Header) so clients get the right MIME type and download filename. - Backpressure keeps memory flat when clients are slow — this is the whole reason to stream.
- Attach
setErrorHandler()for read failures after headers are sent. - Use
@Res({ passthrough: true })when you need raw response access while keeping Nest's piping.
Questions Fréquemment Posées
La leçon « Diffuser de grandes réponses avec StreamableFile » est-elle gratuite ?
Oui — le texte complet de « Diffuser de grandes réponses avec StreamableFile » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours NestJS Enterprise Backend APIs, passe à CoddyKit PRO. Le cours NestJS Enterprise Backend APIs comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « Diffuser de grandes réponses avec StreamableFile » ?
Servez efficacement les téléchargements volumineux avec StreamableFile et les flux lisibles de Node afin d’éviter la mise en mémoire tampon. Tu pratiques NestJS Enterprise Backend APIs avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.
Dois-je avoir de l'expérience pour commencer NestJS Enterprise Backend APIs ?
Aucune expérience préalable n'est requise. NestJS Enterprise Backend APIs sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 2 sur 4.
Combien de temps prend la leçon « Diffuser de grandes réponses avec StreamableFile » ?
La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.
Peux-tu écrire et exécuter du code dans cette leçon NestJS Enterprise Backend APIs ?
Oui. Chaque leçon NestJS Enterprise Backend APIs inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.
Toutes les leçons de ce cours
- Téléversements multiparties avec les intercepteurs Multer
- Diffuser de grandes réponses avec StreamableFile
- Téléversements directs vers S3 avec des URL présignées
- Pipelines de traitement d’images avec Sharp