Lesson 21-NestJS File Handling

File Upload

In NestJS v10, handling file uploads is a common requirement and can be easily implemented using decorators from the @nestjs/common and @nestjs/platform-express modules.

Basic Example

First, ensure the @nestjs/platform-express module is installed, as it includes the decorators needed for file uploads.

npm install @nestjs/platform-express

Controller Example:

import { Controller, Post, UseInterceptors, UploadedFile, UploadedFiles } from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';

@Controller('upload')
export class UploadController {
  @Post()
  @UseInterceptors(FileInterceptor('file'))
  uploadSingle(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
    return { message: 'File has been uploaded successfully.' };
  }

  @Post('multiple')
  @UseInterceptors(FilesInterceptor('files'))
  uploadMultiple(@UploadedFiles() files: Array<Express.Multer.File>) {
    console.log(files);
    return { message: `${files.length} files have been uploaded successfully.` };
  }
}

File Validation

You can use multer‘s storage engine and file filter to validate file types, sizes, etc.

const storage = diskStorage({
  destination: './uploads',
  filename: (req, file, callback) => {
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    callback(null, file.fieldname + '-' + uniqueSuffix);
  },
});

const fileFilter = (req, file, callback) => {
  if (file.mimetype.startsWith('image')) {
    callback(null, true);
  } else {
    callback(new Error('Only image files are allowed!'), false);
  }
};

// Use the storage and filter in the interceptor
@UseInterceptors(FileInterceptor('file', { storage: storage, fileFilter: fileFilter }))

File Arrays

When uploading multiple files, use FilesInterceptor.

import { Controller, Post, UseInterceptors, UploadedFiles } from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';

@Controller('upload')
export class UploadController {
  @Post('multiple')
  @UseInterceptors(FilesInterceptor('files'))
  uploadMultiple(@UploadedFiles() files: Array<Express.Multer.File>) {
    console.log(files);
    return { message: `${files.length} files have been uploaded successfully.` };
  }
}

Multiple Files

Handling multiple file uploads is similar to file arrays but may require additional logic to process each file.

import { Controller, Post, UseInterceptors, UploadedFiles } from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';

@Controller('upload')
export class UploadController {
  @Post('multiple')
  @UseInterceptors(FilesInterceptor('files'))
  uploadMultiple(@UploadedFiles() files: Array<Express.Multer.File>) {
    const successMessages = files.map(file => ({
      fileName: file.originalname,
      message: 'File has been uploaded successfully.',
    }));
    return successMessages;
  }
}

Any File Type

By default, multer does not restrict file types. To accept any file type, simply omit the file filter.

import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('upload')
export class UploadController {
  @Post()
  @UseInterceptors(FileInterceptor('file'))
  uploadSingle(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
    return { message: 'File has been uploaded successfully.' };
  }
}

No File

If no file is included in the request, multer handles this automatically, and @UploadedFile() or @UploadedFiles() will be undefined or an empty array.

import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('upload')
export class UploadController {
  @Post()
  @UseInterceptors(FileInterceptor('file'))
  uploadSingle(@UploadedFile() file: Express.Multer.File) {
    if (!file) {
      return { message: 'No file was uploaded.' };
    }
    console.log(file);
    return { message: 'File has been uploaded successfully.' };
  }
}

Default Options

You can set default multer options globally in main.ts, such as storage location or filename generation strategy.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MulterModule } from '@nestjs/platform-express';
import * as path from 'path';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(
    MulterModule.register({
      dest: path.join(__dirname, '../Uploads'),
    }),
  );
  await app.listen(3000);
}
bootstrap();

Asynchronous Configuration

For more complex file handling logic, you can use asynchronous operations within interceptors. Note that multer‘s core operations (e.g., storage) are already asynchronous, so additional async configuration is usually unnecessary.

import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('upload')
export class UploadController {
  @Post()
  @UseInterceptors(FileInterceptor('file'))
  async uploadSingle(@UploadedFile() file: Express.Multer.File) {
    if (!file) {
      return { message: 'No file was uploaded.' };
    }
    // Process file asynchronously
    const result = await this.processFileAsync(file);
    return result;
  }

  private async processFileAsync(file: Express.Multer.File): Promise<any> {
    // Asynchronous processing logic
    return { message: 'File has been processed successfully.' };
  }
}

Streaming Files

In NestJS v10, handling streaming files is an efficient approach, especially for large files or scenarios requiring processing while downloading. NestJS integrates well with Node.js’s native stream module and supports cross-platform functionality.

Streaming File Handling Basics

Node.js’s stream module provides Readable Streams, Writable Streams, Duplex Streams, and Transform Streams. In NestJS, you typically encounter scenarios like receiving streaming file uploads or sending streaming responses for downloads.

Receiving Streaming File Uploads

For large file uploads, you can use multer‘s memory storage limits to avoid loading the entire file into memory, processing the file stream directly instead.

import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('upload')
export class UploadController {
  @Post('stream')
  @UseInterceptors(FileInterceptor('file', { memoryStorage: true }))
  uploadStream(@UploadedFile() file: Express.Multer.File) {
    // file.stream is a ReadableStream type and can be processed directly
    console.log('File is being streamed.');
    // Further process the stream, e.g., save to a database or cloud storage
  }
}

Sending Streaming File Downloads

When sending files to clients, you can use Node.js’s fs.createReadStream() to create a readable stream and send it via the NestJS response object.

import { Controller, Get, Res } from '@nestjs/common';
import { createReadStream } from 'fs';
import { join } from 'path';

@Controller('download')
export class DownloadController {
  @Get('stream')
  downloadAsStream(@Res() res) {
    const filePath = join(__dirname, '..', 'files', 'example.txt');
    const fileStream = createReadStream(filePath);

    res.setHeader('Content-Type', 'application/octet-stream');
    res.setHeader('Content-Disposition', 'attachment; filename="example.txt"');

    fileStream.pipe(res);
  }
}

Cross-Platform Support

Since NestJS is built on Node.js, and Node.js’s stream API is cross-platform, the methods for handling streaming files are consistent across Windows, macOS, Linux, and other operating systems. Ensure proper file path handling (e.g., using Node.js’s path module for cross-platform paths) to ensure seamless operation.

Advanced Streaming File Classes

In some scenarios, you may need to create custom streaming file classes to implement complex stream processing logic. This typically involves extending Node.js’s stream classes, such as stream.Readable or stream.Writable.

import { Readable } from 'stream';

class MyReadableStream extends Readable {
  constructor(private data: Buffer[]) {
    super();
  }

  _read() {
    for (const chunk of this.data) {
      this.push(chunk);
    }
    this.push(null); // Indicate end of data
  }
}

@Controller('custom-stream')
export class CustomStreamController {
  @Get()
  sendCustomStream(@Res() res) {
    const myStream = new MyReadableStream([Buffer.from('Hello '), Buffer.from('World')]);
    res.setHeader('Content-Type', 'text/plain');
    myStream.pipe(res);
  }
}
Share your love