Streaming de respuestas grandes con StreamableFile
Sirva descargas grandes de forma eficiente mediante StreamableFile y streams legibles de Node, evitando el almacenamiento en búfer.
Streaming de respuestas grandes con StreamableFile es una lección gratuita de NestJS Enterprise Backend APIs en CoddyKit. Esta es la lección 2 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de NestJS Enterprise Backend APIs, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de NestJS Enterprise Backend APIs incluye 4 lecciones en total.
Partes de esta lección aún no han sido traducidas y se muestran en inglés.
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.
Preguntas frecuentes
¿La lección «Streaming de respuestas grandes con StreamableFile» es gratis?
Sí — el texto completo de «Streaming de respuestas grandes con StreamableFile» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de NestJS Enterprise Backend APIs, actualiza a CoddyKit PRO. El curso de NestJS Enterprise Backend APIs incluye 4 lecciones en total.
¿Qué aprenderé en «Streaming de respuestas grandes con StreamableFile»?
Sirva descargas grandes de forma eficiente mediante StreamableFile y streams legibles de Node, evitando el almacenamiento en búfer. Practicas NestJS Enterprise Backend APIs con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar NestJS Enterprise Backend APIs?
No se requiere experiencia previa. NestJS Enterprise Backend APIs en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 2 de 4.
¿Cuánto tiempo toma la lección «Streaming de respuestas grandes con StreamableFile»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de NestJS Enterprise Backend APIs?
Sí. Cada lección de NestJS Enterprise Backend APIs incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- Cargas multipart con interceptors de Multer
- Streaming de respuestas grandes con StreamableFile
- Cargas directas a S3 con URL prefirmadas
- Pipelines de procesamiento de imágenes con Sharp