Nestjs file upload like a Pro [in depth]

tkssharmatkssharma
4 min read

NestJS File Uploads to AWS S3 with Validation: A Comprehensive Guide

Introduction

File uploads are a common feature in many web applications. NestJS, a progressive Node.js framework, provides a convenient way to handle file uploads and integrate with cloud storage services like AWS S3. In this blog post, we'll explore how to implement file uploads with validation in NestJS and store the uploaded files on AWS S3. sample example i am showing here --

Prerequisites

  • A NestJS project set up.
  • An AWS account with an S3 bucket created.
  • AWS credentials configured in your project's environment variables.

Steps:

  1. Install Required Packages:

    npm install @nestjs/platform-express @nestjs/aws-sdk/s3 multer
    
  2. Create an S3 Service:

    import { Injectable } from '@nestjs/common';
    import { S3 } from '@nestjs/aws-sdk/s3';
    
    @Injectable()
    export class S3Service {
      constructor(private readonly s3: S3) {}
    
      async uploadFile(file: Express.Multer.File): Promise<string> {
        const params = {
          Bucket: 'your-bucket-name',
          Key: file.originalname,
          Body: file.buffer,
          ContentType: file.mimetype,
        };
    
        const result = await this.s3.upload(params).promise();
        return result.Location;
      }
    }
    
  3. Configure Multer:

    import { Module } from '@nestjs/common';
    import { MulterModule } from '@nestjs/platform-express';
    
    @Module({
      imports: [
        MulterModule.register({
          storage: multer.diskStorage({
            destination: './uploads',
            filename: (req, file, cb) => {
              cb(null, `${file.originalname}-${Date.now()}`);
            },
          }),
        }),
      ],
    })
    export class UploadModule {}
    
  4. Create a Controller:

    import { Controller, Post, UseInterceptors, UploadedFile, UploadedFiles } from '@nestjs/common';
    import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
    import { S3Service } from './s3.service';
    
    @Controller('upload')
    export class UploadController {
      constructor(private readonly s3Service: S3Service) {}
    
      @Post('single')
      @UseInterceptors(FileInterceptor('file'))
      async uploadSingleFile(@UploadedFile() file: Express.Multer.File) {
        const url = await this.s3Service.uploadFile(file);
        return { url };
      }
    
      @Post('multiple')
      @UseInterceptors(FilesInterceptor('files'))
      async uploadMultipleFiles(@UploadedFiles() files: Express.Multer.File[]) {
        const urls = await Promise.all(files.map(file => this.s3Service.uploadFile(file)));
        return { urls };
      }
    }
    

Code example with Validation

// Native.
/* eslint-disable no-useless-escape */

// Package.
import {
  Body,
  Controller,
  Get,
  HttpCode,
  HttpStatus,
  UploadedFile,
  UseInterceptors,
  ParseFilePipeBuilder,
  Param,
  Post,
  Query,
  UseGuards,
  UsePipes,
  ValidationPipe,
  Req,
  BadRequestException,
  UploadedFiles,
} from '@nestjs/common';
import {
  ApiBearerAuth,
  ApiConsumes,
  ApiCreatedResponse,
  ApiForbiddenResponse,
  ApiInternalServerErrorResponse,
  ApiNotFoundResponse,
  ApiOkResponse,
  ApiOperation,
  ApiTags,
  ApiUnprocessableEntityResponse,
} from '@nestjs/swagger';
import { INTERNAL_SERVER_ERROR, NO_ENTITY_FOUND } from '../../../app.constants';
import { RestaurantService } from '../services/restaurant.service';
import { Type } from 'class-transformer';
import {
  CreateRestaurantBodyDto,
  SearchQueryDto,
  getRestaurantByIdDto,
} from '../dto/restaurant.dto';
import { RolesAllowed } from '../../../core/decorator/role.decorator';
import { Roles } from '../../../core/roles';
import { RolesGuard } from '../../../core/guard/role.guard';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import {
  uploadFile,
  uploadFiles,
} from '../../../core/decorator/file.decorator';
import { imageFileFilter } from '../../../core/filters/file.filter';
import { GetCurrentUser, User, UserData, UserMetaData } from '../interface';

const SIZE = 2 * 1024 * 1024;
const VALID_UPLOADS_MIME_TYPES = ['image/jpeg', 'image/png'];

@ApiBearerAuth('authorization')
@Controller('restaurants')
@UseGuards(RolesGuard)
@UsePipes(
  new ValidationPipe({
    whitelist: true,
    transform: true,
  }),
)
@ApiTags('restaurant')
export class RestaurantController {
  constructor(private readonly service: RestaurantService) {}


  // multiple file upload
  @UseGuards(RolesGuard)
  // add all roles which we want to allow
  @RolesAllowed(Roles['admin'])
  @HttpCode(HttpStatus.OK)
  @ApiConsumes('application/json')
  @ApiNotFoundResponse({ description: NO_ENTITY_FOUND })
  @ApiForbiddenResponse({ description: 'UNAUTHORIZED_REQUEST' })
  @ApiUnprocessableEntityResponse({ description: 'BAD_REQUEST' })
  @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR })
  @ApiOperation({
    description: 'search restaurants based on lat/lon',
  })
  @ApiOkResponse({
    description: 'return search restaurants successfully',
  })
  @Get('/:id')
  public async getRestaurantById(@Param() param: getRestaurantByIdDto) {
    return await this.service.getRestaurantById(param);
  }

  // multiple file upload
  @UseGuards(RolesGuard)
  // add all roles which we want to allow
  @RolesAllowed(Roles['admin'])
  // one file upload
  @Post('one-file')
  // custom decorator
  // ONE FILE UPLOAD ONLY
  @uploadFile('file')
  @ApiForbiddenResponse({ description: 'UNAUTHORIZED_REQUEST' })
  @ApiUnprocessableEntityResponse({ description: 'BAD_REQUEST' })
  @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR })
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  public async uploadFileOne(
    @UploadedFile(
      new ParseFilePipeBuilder()
        .addFileTypeValidator({ fileType: /(jpg|png)$/ })
        .addMaxSizeValidator({ maxSize: SIZE })
        .build({
          errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
        }),
    )
    file: Express.Multer.File,
  ) {
    try {
      return file;
    } catch (err) {
      throw err;
    }
  }

  // multiple file upload
  @UseGuards(RolesGuard)
  // add all roles which we want to allow
  // upload many files together P
  @RolesAllowed(Roles['admin'])
  @Post('many-files')
  // custom decorator
  @uploadFiles('file')
  @ApiForbiddenResponse({ description: 'UNAUTHORIZED_REQUEST' })
  @ApiUnprocessableEntityResponse({ description: 'BAD_REQUEST' })
  @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR })
  @UseInterceptors(FilesInterceptor('file'))
  // all plural here
  // validation with many files
  @ApiConsumes('multipart/form-data')
  public async uploadFiles(
    @UploadedFiles(
      // it will validate each and every files
      new ParseFilePipeBuilder()
        .addFileTypeValidator({ fileType: /(jpg|png)$/ })
        .addMaxSizeValidator({ maxSize: SIZE })
        .build({
          errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
        }),
    )
    files: Array<Express.Multer.File>,
  ) {
    try {
      // once we have files lets upload them in a Loop
      console.log(files);
      return await this.service.upload(files);
    } catch (err) {
      throw err;
    }
  }

  // multiple file upload
  @UseGuards(RolesGuard)
  // add all roles which we want to allow
  @RolesAllowed(Roles['admin'])
  @Post('custom-validation')
  // custom decorator
  @uploadFile('file')
  @ApiForbiddenResponse({ description: 'UNAUTHORIZED_REQUEST' })
  @ApiUnprocessableEntityResponse({ description: 'BAD_REQUEST' })
  @ApiInternalServerErrorResponse({ description: INTERNAL_SERVER_ERROR })
  @UseInterceptors(
    FileInterceptor('file', {
      fileFilter: imageFileFilter,
    }),
  )
  @ApiConsumes('multipart/form-data')
  public async uploadFilev2(@Req() req: any, @UploadedFile() file) {
    console.log(file);
    if (!file || req.fileValidationError) {
      throw new BadRequestException(
        'invalid file provided, allowed *.pdf single file for Invoice',
      );
    }
    return file;
  }
}
  1. Validation:
    • Use custom validators or third-party libraries like class-validator to validate uploaded files.
    • For example, you can check file size, type, and format.

Additional Considerations:

  • Error handling: Implement proper error handling to catch exceptions and provide informative messages.
  • Security: Ensure proper security measures to prevent unauthorized access to uploaded files.
  • Performance: Consider optimizing file uploads for large files or high traffic.
  • Scalability: Evaluate your storage solution to ensure it can handle future growth.

By following these steps and incorporating validation, you can effectively handle file uploads in your NestJS applications and store them securely on AWS S3.

Part-1 %[https://www.youtube.com/watch?v=hsKYAVImMgg ]

Part-2 %[https://www.youtube.com/watch?v=0_bMzkvgIoc]

Playlist %[https://www.youtube.com/watch?v=GSoGVlG1MTQ&list=PLIGDNOJWiL1-8hpXEDlD1UrphjmZ9aMT1]

Github https://github.com/tkssharma/nestjs-advanced-2023/tree/main/apps/22-nestjs-file-upload

0
Subscribe to my newsletter

Read articles from tkssharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

tkssharma
tkssharma

I'm a full-stack software developer creating open-source projects and writing about modern JavaScript client-side and server-side. Working remotely from India.