File Uploads, Streaming, Mailing & i18n
Production NestJS applications inevitably need to accept binary uploads, stream large payloads back to clients, send transactional emails, and serve content in multiple languages. This lesson covers all four concerns using first-class NestJS mechanisms: Multer interceptors for uploads, StreamableFile for streaming, @nestjs/mailer for email, and nestjs-i18n for localisation.
File Uploads with FileInterceptor and Multer
NestJS wraps Multer through @nestjs/platform-express. Use FileInterceptor (single file) or FilesInterceptor (multiple) at the route level, then read the upload via @UploadedFile():
import {
Controller, Post, UseInterceptors,
UploadedFile, BadRequestException,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
import { Express } from 'express';
@Controller('uploads')
export class UploadsController {
@Post('avatar')
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: './uploads',
filename: (_req, file, cb) => {
const unique = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, `${unique}${extname(file.originalname)}`);
},
}),
limits: { fileSize: 2 * 1024 * 1024 }, // 2 MB
fileFilter: (_req, file, cb) => {
const allowed = /\.(jpg|jpeg|png|webp)$/i;
if (!allowed.test(file.originalname)) {
return cb(new BadRequestException('Only image files are allowed'), false);
}
cb(null, true);
},
}),
)
uploadAvatar(@UploadedFile() file: Express.Multer.File) {
if (!file) throw new BadRequestException('No file provided');
return { path: file.path, size: file.size };
}
}
Validate both extension and MIME type. file.mimetype is sent by the client and can be spoofed. For high-security contexts, read the first few bytes (magic bytes) with a library like file-type to confirm the real format.
Streaming Responses with StreamableFile
When you need to pipe a file or a Node.js Readable stream back to the client without buffering it entirely in memory, return a StreamableFile. NestJS sets the correct headers and pipes the stream:
import { Controller, Get, Param, Res, StreamableFile } from '@nestjs/common';
import { createReadStream } from 'fs';
import { join } from 'path';
import { Response } from 'express';
@Controller('files')
export class FilesController {
@Get(':filename')
downloadFile(
@Param('filename') filename: string,
@Res({ passthrough: true }) res: Response,
): StreamableFile {
const filePath = join(process.cwd(), 'uploads', filename);
const stream = createReadStream(filePath);
res.set({
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="${filename}"`,
});
return new StreamableFile(stream);
}
}
passthrough: true is required. When you inject @Res(), NestJS normally hands full control to Express. Setting passthrough: true lets you set headers while still allowing NestJS to finalise the response — including piping the StreamableFile.
Sending Email with @nestjs/mailer
Install the package and configure a transport (SMTP, SendGrid, etc.). Use Handlebars templates stored in a templates/ folder for HTML emails:
// app.module.ts (excerpt)
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { join } from 'path';
MailerModule.forRoot({
transport: {
host: process.env.MAIL_HOST,
port: 587,
auth: { user: process.env.MAIL_USER, pass: process.env.MAIL_PASS },
},
defaults: { from: '"No Reply" <noreply@example.com>' },
template: {
dir: join(__dirname, 'templates'),
adapter: new HandlebarsAdapter(),
options: { strict: true },
},
}),
// mail.service.ts
import { Injectable } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';
@Injectable()
export class MailService {
constructor(private mailer: MailerService) {}
async sendWelcome(to: string, name: string): Promise<void> {
await this.mailer.sendMail({
to,
subject: 'Welcome to Our Platform',
template: 'welcome', // templates/welcome.hbs
context: { name },
});
}
}
Never hard-code credentials. Always read MAIL_HOST, MAIL_USER, and MAIL_PASS from environment variables (e.g. via @nestjs/config). Commit a .env.example with placeholder values, never the real .env.
Internationalisation with nestjs-i18n
nestjs-i18n provides decorators, pipes, and a service for serving translated strings. Store translations as JSON files per locale, then inject I18nService or use the @I18nLang() decorator to detect the current language from the Accept-Language header, query param, or cookie:
// i18n/en/common.json → { "WELCOME": "Welcome, {name}!" }
// i18n/ar/common.json → { "WELCOME": "أهلاً، {name}!" }
// app.module.ts (excerpt)
import { I18nModule, AcceptLanguageResolver } from 'nestjs-i18n';
import { join } from 'path';
I18nModule.forRoot({
fallbackLanguage: 'en',
loaderOptions: { path: join(__dirname, '/i18n/'), watch: true },
resolvers: [AcceptLanguageResolver],
}),
// greet.service.ts
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class GreetService {
constructor(private i18n: I18nService) {}
async greet(lang: string, name: string): Promise<string> {
return this.i18n.translate('common.WELCOME', { lang, args: { name } });
}
}
Summary
NestJS provides clean, decorator-driven primitives for all four concerns. FileInterceptor wraps Multer to handle uploads with size and type validation. StreamableFile pipes Node.js streams to clients without memory buffering. @nestjs-modules/mailer integrates with any SMTP provider using Handlebars templates. nestjs-i18n externalises all user-facing strings into locale JSON files, keeping controllers free of hard-coded messages. Combining these makes your API production-ready across language boundaries.