بث الاستجابات الكبيرة باستخدام StreamableFile
قدّم التنزيلات الكبيرة بكفاءة باستخدام StreamableFile وNode readable streams لتجنّب التخزين المؤقت
بث الاستجابات الكبيرة باستخدام 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
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.
الأسئلة الشائعة
هل درس «بث الاستجابات الكبيرة باستخدام StreamableFile» مجاني؟
نعم — نص درس «بث الاستجابات الكبيرة باستخدام StreamableFile» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة NestJS Enterprise Backend APIs، انتقل إلى CoddyKit PRO. تتضمن دورة NestJS Enterprise Backend APIs 4 دروس في المجموع.
ماذا ستتعلم في «بث الاستجابات الكبيرة باستخدام StreamableFile»؟
قدّم التنزيلات الكبيرة بكفاءة باستخدام StreamableFile وNode readable streams لتجنّب التخزين المؤقت تتمرن على 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 يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- عمليات الرفع متعددة الأجزاء باستخدام Multer Interceptors
- بث الاستجابات الكبيرة باستخدام StreamableFile
- عمليات الرفع المباشر إلى S3 باستخدام Presigned URLs
- مسارات معالجة الصور باستخدام Sharp