StreamableFileによる大容量レスポンスのストリーミング
StreamableFileとNodeのreadable streamを使って大容量ダウンロードを効率的に提供し、バッファリングを避けます
「StreamableFileによる大容量レスポンスのストリーミング」はCoddyKit上の無料NestJS Enterprise Backend APIsレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはNestJS Enterprise Backend APIs学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 NestJS Enterprise Backend APIsコースには全4レッスンが含まれています。
このレッスンの一部はまだ翻訳されておらず、英語で表示されています。
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.
よくある質問
「StreamableFileによる大容量レスポンスのストリーミング」レッスンは無料ですか?
はい。「StreamableFileによる大容量レスポンスのストリーミング」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、NestJS Enterprise Backend APIsコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 NestJS Enterprise Backend APIsコースには全4レッスンが含まれています。
「StreamableFileによる大容量レスポンスのストリーミング」で何を学びますか?
StreamableFileとNodeのreadable streamを使って大容量ダウンロードを効率的に提供し、バッファリングを避けます ブラウザで直接実行するハンズオンコードでNestJS Enterprise Backend APIsを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
NestJS Enterprise Backend APIsを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのNestJS Enterprise Backend APIsは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「StreamableFileによる大容量レスポンスのストリーミング」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このNestJS Enterprise Backend APIsレッスンでコードを書いて実行できますか?
はい。すべてのNestJS Enterprise Backend APIsレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- Multer InterceptorによるMultipartアップロード
- StreamableFileによる大容量レスポンスのストリーミング
- Presigned URLによるS3への直接アップロード
- Sharpによる画像処理パイプライン