การสตรีมคำตอบขนาดใหญ่ด้วย StreamableFile
ให้บริการการดาวน์โหลดขนาดใหญ่อย่างมีประสิทธิภาพด้วย StreamableFile และสตรีมที่อ่านได้ของ Node เพื่อหลีกเลี่ยงการเก็บข้อมูลในบัฟเฟอร์
การสตรีมคำตอบขนาดใหญ่ด้วย StreamableFile เป็นบทเรียน NestJS Enterprise Backend APIs ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 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
StreamableFilereturn 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:
typesets theContent-Typeheader.dispositionsetsContent-Dispositionso 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
StreamableFiletype/dispositionoptions 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 thesetErrorHandler()/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: acreateReadStream, an S3 object body, or a generator-backed stream. - Set
typeanddisposition(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.
เรียนรู้ TypeScript ด้วย AI tutor — ฟรี
เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป
- คอร์ส
- 20
- บทเรียน
- 76
คำถามที่พบบ่อย
บทเรียน “การสตรีมคำตอบขนาดใหญ่ด้วย StreamableFile” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การสตรีมคำตอบขนาดใหญ่ด้วย StreamableFile” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส NestJS Enterprise Backend APIs ให้อัปเกรดเป็น CoddyKit PRO คอร์ส NestJS Enterprise Backend APIs มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การสตรีมคำตอบขนาดใหญ่ด้วย StreamableFile”
ให้บริการการดาวน์โหลดขนาดใหญ่อย่างมีประสิทธิภาพด้วย StreamableFile และสตรีมที่อ่านได้ของ Node เพื่อหลีกเลี่ยงการเก็บข้อมูลในบัฟเฟอร์ คุณปฏิบัติ NestJS Enterprise Backend APIs ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การอัปโหลดแบบหลายส่วนด้วยตัวดักรับ Multer
- การสตรีมคำตอบขนาดใหญ่ด้วย StreamableFile
- การอัปโหลดไปยัง S3 โดยตรงด้วย URL ที่ลงลายเซ็นล่วงหน้า
- กระบวนการประมวลผลรูปภาพด้วย Sharp