0Pricing
NestJS Enterprise Backend APIs · レッスン

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 StreamableFile return 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:

  • type sets the Content-Type header.
  • disposition sets Content-Disposition so 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 StreamableFile type/disposition options 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 the setErrorHandler()/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: a createReadStream, an S3 object body, or a generator-backed stream.
  • Set type and disposition (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フィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Multer InterceptorによるMultipartアップロード
  2. StreamableFileによる大容量レスポンスのストリーミング
  3. Presigned URLによるS3への直接アップロード
  4. Sharpによる画像処理パイプライン
← NestJS Enterprise Backend APIsに戻る