0Pricing
NestJS Enterprise Backend APIs · Aula

Transmitindo Respostas Grandes com StreamableFile

Forneça downloads grandes com eficiência usando StreamableFile e fluxos legíveis do Node, evitando o armazenamento em buffer.

Transmitindo Respostas Grandes com StreamableFile é uma aula grátis de NestJS Enterprise Backend APIs no CoddyKit. Esta é a aula 2 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de NestJS Enterprise Backend APIs, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de NestJS Enterprise Backend APIs inclui 4 aulas no total.

Partes desta aula ainda não foram traduzidas e aparecem em inglês.

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.

Perguntas Frequentes

A aula “Transmitindo Respostas Grandes com StreamableFile” é grátis?

Sim — o texto completo de “Transmitindo Respostas Grandes com StreamableFile” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de NestJS Enterprise Backend APIs, atualize para CoddyKit PRO. O curso de NestJS Enterprise Backend APIs inclui 4 aulas no total.

O que vou aprender em “Transmitindo Respostas Grandes com StreamableFile”?

Forneça downloads grandes com eficiência usando StreamableFile e fluxos legíveis do Node, evitando o armazenamento em buffer. Você pratica NestJS Enterprise Backend APIs com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar NestJS Enterprise Backend APIs?

Nenhuma experiência prévia é necessária. NestJS Enterprise Backend APIs no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 2 de 4.

Quanto tempo leva a aula “Transmitindo Respostas Grandes com StreamableFile”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de NestJS Enterprise Backend APIs?

Sim. Cada aula de NestJS Enterprise Backend APIs inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Uploads Multipart com Interceptadores Multer
  2. Transmitindo Respostas Grandes com StreamableFile
  3. Uploads Diretos para S3 com URLs Pré-Assinadas
  4. Pipelines de Processamento de Imagens com Sharp
← Voltar para NestJS Enterprise Backend APIs