0Pricing
NestJS Enterprise Backend APIs · Урок

Потоковая передача больших ответов с StreamableFile

Эффективно отдавайте большие загрузки с помощью StreamableFile и читаемых потоков Node, избегая буферизации

«Потоковая передача больших ответов с StreamableFile» — бесплатный урок NestJS Enterprise Backend APIs на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения 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/7) и разблокировать остальной курс NestJS Enterprise Backend APIs, подпишись на CoddyKit PRO. Курс NestJS Enterprise Backend APIs содержит 4 уроков всего.

Чему я научусь в уроке «Потоковая передача больших ответов с StreamableFile»?

Эффективно отдавайте большие загрузки с помощью StreamableFile и читаемых потоков Node, избегая буферизации Ты практикуешь NestJS Enterprise Backend APIs с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать NestJS Enterprise Backend APIs?

Предыдущий опыт не требуется. NestJS Enterprise Backend APIs на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «Потоковая передача больших ответов с StreamableFile»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке NestJS Enterprise Backend APIs?

Да. Каждый урок NestJS Enterprise Backend APIs включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Составные загрузки с перехватчиками Multer
  2. Потоковая передача больших ответов с StreamableFile
  3. Прямая загрузка в S3 с подписанными URL
  4. Конвейеры обработки изображений с Sharp
← Назад к NestJS Enterprise Backend APIs