Nestjs file upload like a Pro [in depth]

tkssharma - Sep 7 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode
  1. 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;
     }
   }
Enter fullscreen mode Exit fullscreen mode
  1. 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 {}
Enter fullscreen mode Exit fullscreen mode
  1. 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 };
     }
   }
Enter fullscreen mode Exit fullscreen mode

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;
  }
}

Enter fullscreen mode Exit fullscreen mode
  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

Part-2

Playlist

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .