Mastering File Upload and Validation in NestJS with Multer
A Comprehensive Guide for Developers
This guide will show you how to efficiently incorporate file upload and validation into your NestJS project. The tutorial will walk you through the steps of receiving and verifying files using the Multer middleware package with the Express Adapter. By following this guide, you can create a personalized file upload and validation process that works best for your NestJS application.
Accepting files
File processing in NestJs is handled with Multer. It has a built-in Multer middleware package for Express Adapter. The only required package to install is @types/multer
, which is for better type safety.
npm i -D @types/multer @nestjs/platform-express
Start by creating an endpoint for single file uploads.
import { Post, UploadedFile, UseInterceptors, Controller } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('files')
export class FileController {
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log(file);
}
}
Make sure to use
@nestjs/common: "^9.0.0"
, it's required to useUploadedFile
.
If you use swagger, you can specify ApiConsumes
decorator to multipart/form-data
. This tells swagger that this route accepts form data.
import { Post, UploadedFile, UseInterceptors, Controller } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
@ApiTags('Uploding Files')
@Controller('files')
@ApiConsumes('multipart/form-data')
export class FileController {
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log(file);
}
}
When it comes to filing storage, Multer has a default memory setting. While this is a viable choice, it may not suit your needs if you plan to bypass disk storage and instead upload files directly to external storage solutions like Cloudinary or AWS S3.
How to Save Data to Disk Storage
To upload your files to disk storage, you can provide the file destination in the FileInterceptor
. The interceptor accepts Multer Options.
const UPLOAD_DIR = './upload/files/';
@ApiTags('Uploding Files')
@Controller('files')
@ApiConsumes('multipart/form-data')
export class FileController {
@Post('upload')
@UseInterceptors(FileInterceptor('file', { dest: UPLOAD_DIR }))
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log(file);
}
}
The upload directory path is relative to your root directory.
Hey there! When dealing with a lot of users, we want to make sure we avoid any issues that might come up with file names. To prevent this from happening, we recommend assigning a separate folder for each upload. That way, everything stays organized and everyone can easily find what they need.
There is a helpful tip to share with you. Are you aware that every user can have their unique folder in the upload directory? What's more, you can enhance this feature by configuring a storage engine for Multer, which allows you to create a dynamic upload path. Why not give it a try?
import { Post, UploadedFile, UseInterceptors, Controller } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { diskStorage } from 'multer';
import * as path from 'path';
const UPLOAD_DIR = './upload/files/';
// User interface of the authenticated user
interface User {
id: string;
}
/**
* You can use this function to generate a unique filename for each file
* User id is used to generate a unique filename
* The User object can be attached to the request object in the auth middleware
*/
const defaultConfig = diskStorage({
destination: UPLOAD_DIR,
filename: (req: Request & { user: User }, file, cb) => {
const uid = req.user.id;
cb(null, `${uid}${path.extname(file.originalname)}`)
}
})
@ApiTags('Uploding Files')
@Controller('files')
@ApiConsumes('multipart/form-data')
export class FileController {
@Post('upload')
@UseInterceptors(FileInterceptor('file', { storage: defaultConfig }))
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log(file);
}
}
In this example, the user object could be attached to the request manually in the auth middleware.
This is a perfect example of how to accept files and save them in a super cool and unique path!
The file would be saved to disk before
console.log(file)
runs.
The next section provides a way of adding file processing like validation of files uploaded.
Validate Your Files with ParseFilePipe and ParseFilePipeBuilder
Using ParseFilePipe with Validator classes
NestJs Pipes offer an efficient way to validate files. To perform file validation, simply create a validator class such as MaxFileSizeValidator or FileNameValidator, and pass it to the ParseFilePipe.
import { ..., ParseFilePipe } from '@nestjs/common';
...
@Post('upload')
@UseInterceptors(FileInterceptor('file', { storage: defaultConfig }))
uploadFile(
@UploadedFile(
new ParseFilePipe({
validators: [
// ... Set of file validator instances here
]
})
)
file: Express.Multer.File
) {
console.log(file);
}
...
Create a FileValidator
class to use with ParseFilePipe
.
import { FileValidator } from '@nestjs/common';
class MaxFileSize extends FileValidator<{ maxSize: number }>{
constructor(options: { maxSize: number }) {
super(options)
}
isValid(file: Express.Multer.File): boolean | Promise<boolean> {
const in_mb = file.size / 1000000
return in_mb <= this.validationOptions.maxSize
}
buildErrorMessage(): string {
return `File uploaded is too big. Max size is (${this.validationOptions.maxSize} MB)`
}
}
The interfaceFileValidator
has two methods needed to create its class. The method isValid
and buildErrorMessage
.
The method, isValid
will contain your logic that defines if the file is valid or not depending on the options passed in the constructor.
From the constructor definition, maxSize
can be passed as an option and is used in the isValid
method to know what is considered the maximum file size.
/**
* Builds an error message in case the validation fails.
* @param file the file from the request object
*/
abstract buildErrorMessage(file: any): string;
Above is the interface of the buildErrorMessage
function. It's used to build unique or general error messages for uploaded files.
Pass an instance of the MaxFileSize
class in the ParseFilePipe
. The maxSize
can be passed to the constructor during instantiation.
...
const MAX_UPLOAD_SIZE = 10; // in MB
...
@Post('upload')
@UseInterceptors(FileInterceptor('file', { storage: defaultConfig }))
uploadFile(
@UploadedFile(
new ParseFilePipe({
validators: [
// ... Set of file validator instances here
new MaxFileSize({
maxSize: MAX_UPLOAD_SIZE
}),
]
})
)
file: Express.Multer.File
) {
console.log(file);
}
...
The code should look like this now
import { Post, UploadedFile, UseInterceptors, Controller, ParseFilePipe } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { diskStorage } from 'multer';
import * as path from 'path';
import { FileValidator } from '@nestjs/common';
const UPLOAD_DIR = './upload/files/';
const MAX_UPLOAD_SIZE = 10; // in MB
// User interface of the authenticated user
interface User {
id: string;
}
class MaxFileSize extends FileValidator<{ maxSize: number }>{
constructor(options: { maxSize: number }) {
super(options)
}
isValid(file: Express.Multer.File): boolean | Promise<boolean> {
const in_mb = file.size / 1000000
return in_mb <= this.validationOptions.maxSize
}
buildErrorMessage(): string {
return `File uploaded is too big. Max size is (${this.validationOptions.maxSize} MB)`
}
}
/**
* You can use this function to generate a unique filename for each file
* User id is used to generate a unique filename
* The User object can be attached to the request object in the auth middleware
*/
const defaultConfig = diskStorage({
destination: UPLOAD_DIR,
filename: (req: Request & { user: User }, file, cb) => {
const uid = req.user.id;
cb(null, `${uid}${path.extname(file.originalname)}`)
}
})
@ApiTags('Uploding Files')
@Controller('files')
@ApiConsumes('multipart/form-data')
export class FileController {
@Post('upload')
@UseInterceptors(FileInterceptor('file', { storage: defaultConfig }))
uploadFile(
@UploadedFile(
new ParseFilePipe({
validators: [
// ... Set of file validator instances here
new MaxFileSize({
maxSize: MAX_UPLOAD_SIZE
}),
]
})
)
file: Express.Multer.File
) {
console.log(file);
}
}
If your validators are getting much, you can create them in a separate file and import them here as a named constant like
myValidators.
- NestJs Docs
Using ParseFilePipeBuilder
Wow, this is amazing! It's so much better because it comes with built-in validators for file size and type! Plus, there's a way to add even more validators using the addValidator method for complex file validation! How cool is that?!
import { ..., ParseFilePipeBuilder, HttpStatus } from '@nestjs/common';
...
@Post('upload')
@UseInterceptors(FileInterceptor('file', { storage: defaultConfig }))
uploadFile(
@UploadedFile(
new ParseFilePipeBuilder()
.addFileTypeValidator({
fileType: /(jpg|jpeg|png|gif)$/,
})
.addMaxSizeValidator({
maxSize: 1000
})
.addValidator(
new MaxFileSize({
maxSize: MAX_UPLOAD_SIZE
}),
)
.build({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY
})
)
file: Express.Multer.File
) {
console.log(file);
}
...
errorHttpStatusCode
is the HTTP status code you want to be returned when validation fails.
Simplify the process of file validation with the help of ParseFilePipeBuilder. For straightforward validations like file types and sizes, this tool is highly recommended. However, for more intricate validations, it's best to create file validator classes.
Optional File Uploads
File upload parameters can be made optional or not required by passing an extra option in the ParseFilePipeBuilder
build method.
new ParseFilePipeBuilder()
.addFileTypeValidator({
fileType: "png",
})
.build({
fileIsRequired: false // It's required by default
})
Accepting multiple files
Simply change the file interceptor and upload the file decorator.
import { UploadedFiles } from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
const MAX_FILES_COUNT = 10;
...
@Post('upload/multiple')
@UseInterceptors(
FilesInterceptor('files', MAX_FILES_COUNT, { storage: defaultConfig })
)
uploadFiles(
@UploadedFiles(
new ParseFilePipeBuilder()
.addFileTypeValidator({
fileType: /(jpg|jpeg|png|gif)$/,
})
.addMaxSizeValidator({
maxSize: 1000
})
.addValidator(
new MaxFileSize({
maxSize: MAX_UPLOAD_SIZE
}),
)
.build({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY
})
)
files: Express.Multer.File[]
) {
console.log(files);
}
...
It's now UploadedFiles
and FilesInterceptor
. FilesInterceptor
accepts max files number that can be uploaded through the endpoint.
Make sure to accept files as an argument, not just file.
Below, you can find the complete code.
import { Post, UploadedFile, UseInterceptors, Controller, ParseFilePipe, ParseFilePipeBuilder, HttpStatus, UploadedFiles } from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { diskStorage } from 'multer';
import * as path from 'path';
import { FileValidator } from '@nestjs/common';
const UPLOAD_DIR = './upload/files/';
const MAX_UPLOAD_SIZE = 10; // in MB
const MAX_FILES_COUNT = 10; // Maximum number of files that can be uploaded at once
// User interface of the authenticated user
interface User {
id: string;
}
class MaxFileSize extends FileValidator<{ maxSize: number }>{
constructor(options: { maxSize: number }) {
super(options)
}
isValid(file: Express.Multer.File): boolean | Promise<boolean> {
const in_mb = file.size / 1000000
return in_mb <= this.validationOptions.maxSize
}
buildErrorMessage(): string {
return `File uploaded is too big. Max size is (${this.validationOptions.maxSize} MB)`
}
}
/**
* You can use this function to generate a unique filename for each file
* User id is used to generate a unique filename
* The User object can be attached to the request object in the auth middleware
*/
const defaultConfig = diskStorage({
destination: UPLOAD_DIR,
filename: (req: Request & { user: User }, file, cb) => {
const uid = req.user.id;
cb(null, `${uid}${path.extname(file.originalname)}`)
}
})
@ApiTags('Uploding Files')
@Controller('files')
@ApiConsumes('multipart/form-data')
export class FileController {
@Post('upload')
@UseInterceptors(FileInterceptor('file', { storage: defaultConfig }))
uploadFile(
@UploadedFile(
new ParseFilePipeBuilder()
.addFileTypeValidator({
fileType: /(jpg|jpeg|png|gif)$/,
})
.addMaxSizeValidator({
maxSize: 1000
})
.build({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY
})
)
file: Express.Multer.File
) {
console.log(file);
}
@Post('upload/multiple')
@UseInterceptors(
FilesInterceptor('files', MAX_FILES_COUNT, { storage: defaultConfig })
)
uploadFiles(
@UploadedFiles(
new ParseFilePipeBuilder()
.addFileTypeValidator({
fileType: /(jpg|jpeg|png|gif)$/,
})
.addValidator(
new MaxFileSize({
maxSize: MAX_UPLOAD_SIZE
}),
)
.build({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY
})
)
files: Express.Multer.File[]
) {
console.log(files);
}
}
For a comprehensive configuration of Multer file upload, refer to the NestJs File Upload Documentation. Read on to learn more.
Conclusion
In conclusion, this article provides a comprehensive guide on how to implement file upload and validation in NestJS using the Multer middleware package for the Express Adapter. It covers accepting and validating files, saving files to disk storage, file validation with ParseFilePipe and ParseFilePipeBuilder, and accepting multiple files. By following the steps outlined in this article, developers can easily design their custom file upload and validation flow in NestJS.