Nestjs file upload like a Pro [in depth]
tkssharma
Posted on September 7, 2024
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:
- Install Required Packages:
npm install @nestjs/platform-express @nestjs/aws-sdk/s3 multer
- 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;
}
}
- 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 {}
- 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;
}
}
-
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.
- Use custom validators or third-party libraries like
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
Posted on September 7, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.