typescript 41 lines · 7 steps

Streaming CSV file downloads in NestJS

A NestJS controller streams a report file to the client without buffering it in memory.

Explained by highlit
1import { Controller, Get, Param, StreamableFile, Res, NotFoundException, Header } from '@nestjs/common';
2import { Response } from 'express';
3import { createReadStream, promises as fsp } from 'fs';
4import { join } from 'path';
5import { ReportsService } from './reports.service';
6 
7@Controller('reports')
8export class ReportsController {
9 constructor(private readonly reportsService: ReportsService) {}
10 
11 @Get(':id/export')
12 @Header('Content-Type', 'text/csv')
13 async export(
14 @Param('id') id: string,
15 @Res({ passthrough: true }) res: Response,
16 ): Promise<StreamableFile> {
17 const report = await this.reportsService.findExport(id);
18 if (!report) {
19 throw new NotFoundException(`Report ${id} not found`);
20 }
21 
22 const filePath = join(this.reportsService.exportDir, report.fileName);
23 
24 let stat;
25 try {
26 stat = await fsp.stat(filePath);
27 } catch {
28 throw new NotFoundException('Export file is no longer available');
29 }
30 
31 res.set({
32 'Content-Length': stat.size.toString(),
33 'Content-Disposition': `attachment; filename="${report.fileName}"`,
34 });
35 
36 const stream = createReadStream(filePath);
37 return new StreamableFile(stream, {
38 disposition: `attachment; filename="${report.fileName}"`,
39 });
40 }
41}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1StreamableFile lets you send large files as a stream instead of loading them fully into memory.
  2. 2Using passthrough:true keeps NestJS in control of the response while you still set custom headers manually.
  3. 3Validating both the database record and the file on disk avoids serving broken or missing downloads.

Related explainers

ruby
class Order
  class InvalidTransition < StandardError; end
 
  TRANSITIONS = {

A state machine for order transitions in Ruby

state-machine data-driven error-handling
Intermediate 8 steps
typescript
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';

How a JWT strategy authenticates in NestJS

authentication jwt passport
Intermediate 7 steps
typescript
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';

Refreshing auth tokens in an Angular interceptor

http-interceptor token-refresh rxjs
Advanced 8 steps
rust
use std::cmp::Ordering;
use std::str::FromStr;
 
#[derive(Debug, Clone, PartialEq, Eq)]

Parsing and ordering semantic versions in Rust

parsing trait-implementation ordering
Intermediate 8 steps
javascript
'use client';
 
import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';

How a Next.js error boundary recovers

error-boundary error-handling observability
Intermediate 8 steps
ruby
class Registration < ApplicationRecord
  belongs_to :event
 
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }

Validating registrations in Rails

validations i18n error-handling
Intermediate 8 steps

Share this explainer

Here's the card — post it anywhere.

Streaming CSV file downloads in NestJS — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code