0Pricing
NestJS Enterprise Backend APIs · 课时

使用 StreamableFile 流式传输大型响应

使用 StreamableFile 和 Node 可读流高效提供大型下载,避免缓冲。

使用 StreamableFile 流式传输大型响应 是 CoddyKit 上的免费 NestJS Enterprise Backend APIs 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 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 流式传输大型响应」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 NestJS Enterprise Backend APIs 课程的其余内容,请升级到 CoddyKit PRO。 NestJS Enterprise Backend APIs 课程共包含 4 节课。

「使用 StreamableFile 流式传输大型响应」这节课中我会学到什么?

使用 StreamableFile 和 Node 可读流高效提供大型下载,避免缓冲。 你通过在浏览器中直接运行的动手代码来练习 NestJS Enterprise Backend APIs,全天候 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 拦截器处理多部分上传
  2. 使用 StreamableFile 流式传输大型响应
  3. 使用预签名 URL 直传 S3
  4. 使用 Sharp 构建图像处理流程
← 返回 NestJS Enterprise Backend APIs