0Pricing
NestJS Enterprise Backend APIs · Pelajaran

Mengalirkan Respons Besar dengan StreamableFile

Sajikan unduhan besar secara efisien menggunakan StreamableFile dan aliran Node yang dapat dibaca agar tidak perlu melakukan buffering.

Mengalirkan Respons Besar dengan StreamableFile adalah pelajaran NestJS Enterprise Backend APIs gratis di CoddyKit. Ini adalah pelajaran 2 dari 4. Kamu bisa membaca pelajaran lengkapnya di bawah secara gratis — lalu praktikkan langsung di browser dengan editor kode bawaan dan tutor AI 24/7. Ini adalah bagian dari jalur belajar NestJS Enterprise Backend APIs, dan progresmu tersinkronisasi di web dan aplikasi CoddyKit. Kursus NestJS Enterprise Backend APIs mencakup 4 pelajaran total.

Bagian dari pelajaran ini belum diterjemahkan dan ditampilkan dalam bahasa Inggris.

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.

Pertanyaan yang Sering Diajukan

Apakah pelajaran “Mengalirkan Respons Besar dengan StreamableFile” gratis?

Ya — teks lengkap “Mengalirkan Respons Besar dengan StreamableFile” gratis dibaca di sini di web. Untuk praktiknya secara interaktif (editor kode bawaan dan tutor AI 24/7) dan buka sisa kursus NestJS Enterprise Backend APIs, upgrade ke CoddyKit PRO. Kursus NestJS Enterprise Backend APIs mencakup 4 pelajaran total.

Apa yang akan aku pelajari di “Mengalirkan Respons Besar dengan StreamableFile”?

Sajikan unduhan besar secara efisien menggunakan StreamableFile dan aliran Node yang dapat dibaca agar tidak perlu melakukan buffering. Kamu berlatih NestJS Enterprise Backend APIs dengan kode praktik yang langsung kamu jalankan di browser, dan tutor AI 24/7 menjawab pertanyaanmu saat kamu mengerjakan pelajaran ini.

Apakah aku perlu pengalaman untuk memulai NestJS Enterprise Backend APIs?

Tidak diperlukan pengalaman sebelumnya. NestJS Enterprise Backend APIs di CoddyKit dirancang untuk pemula hingga pelajar tingkat lanjut, jadi kamu bisa memulai di sini atau dari awal dan belajar sesuai kecepatan kamu sendiri. Ini adalah pelajaran 2 dari 4.

Berapa lama pelajaran “Mengalirkan Respons Besar dengan StreamableFile” memakan waktu?

Sebagian besar pelajaran CoddyKit memakan waktu sekitar 5–10 menit. Setiap pelajaran ringkas dan interaktif, jadi kamu membuat kemajuan stabil dan melanjutkan dari tempat kamu tinggalkan di web dan aplikasi.

Bisakah aku menulis dan menjalankan kode dalam pelajaran NestJS Enterprise Backend APIs ini?

Ya. Setiap pelajaran NestJS Enterprise Backend APIs menyertakan editor kode bawaan, jadi kamu menulis dan menjalankan kode nyata langsung di browser dan mendapatkan umpan balik AI instan — tidak diperlukan penyiapan lokal.

Semua pelajaran dalam kursus ini

  1. Unggahan Multipart dengan Pencegat Multer
  2. Mengalirkan Respons Besar dengan StreamableFile
  3. Unggahan Langsung ke S3 dengan URL Bertanda Tangan
  4. Alur Pemrosesan Gambar dengan Sharp
← Kembali ke NestJS Enterprise Backend APIs