Pipe Large Payloads Without Buffering
Streaming Responses
Return a StreamableFile to stream binary payloads, or grab the response to pipe a Node stream directly.
What you'll learn
- Return a `StreamableFile` from a controller
- Stream a file from disk
- Set `Content-Disposition` for downloads
When a response could be tens of megabytes (a PDF, a video, a CSV export)
you don’t want to load it all into memory and then send. Nest’s
StreamableFile lets you return a stream and Nest pipes it for you.
Returning a StreamableFile
import { createReadStream } from 'node:fs';
import { join } from 'node:path';
import { Controller, Get, StreamableFile } from '@nestjs/common';
@Controller('files')
export class FilesController {
@Get('report')
download(): StreamableFile {
const file = createReadStream(join(process.cwd(), 'report.pdf'));
return new StreamableFile(file);
}
} Nest pipes the read stream to the response. No buffering, no memory spikes, and back-pressure works correctly.
Setting Content-Type and Disposition
StreamableFile accepts options for the common headers:
return new StreamableFile(file, {
type: 'application/pdf',
disposition: 'attachment; filename="quarterly.pdf"',
length: stat.size,
}); type sets Content-Type, disposition triggers a download in the
browser, and length (optional but recommended) lets the client show a
progress bar.
Streaming Generated Content
You don’t need a file on disk — any Node Readable works. A common
trick is generating a CSV row by row:
import { Readable } from 'node:stream';
@Get('users.csv')
export() {
async function* rows() {
yield 'id,name\n';
for await (const u of this.users.cursor()) {
yield `${u.id},${u.name}\n`;
}
}
return new StreamableFile(Readable.from(rows.call(this)), {
type: 'text/csv',
disposition: 'attachment; filename="users.csv"',
});
} That stays memory-stable even for millions of rows.
Going Lower-Level With @Res()
If you need fine control (res.write(), custom flush points, SSE-like
behavior), drop into the raw response — but you lose Nest’s
auto-serialization:
import { Res } from '@nestjs/common';
import type { Response } from 'express';
@Get('live')
live(@Res() res: Response) {
res.setHeader('Content-Type', 'text/plain');
res.write('starting…\n');
setTimeout(() => res.end('done\n'), 1000);
} Reach for this only when StreamableFile truly can’t express what you
need. For server-sent events, the dedicated @Sse() decorator is even
better.
Watch Out For
- Don’t
awaitthe whole stream before returning — that defeats the purpose. Hand Nest the readable, let it pipe. - Error handling — if the stream errors mid-flight, you can’t change the status code (headers are already sent). Make sure the source is validated before you start piping.