DEV Community

Israel Ayanwola
Israel Ayanwola

Posted on • Originally published at thecodeway.hashnode.dev on

Mastering File Upload and Validation in NestJS with Multer

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Make sure to use @nestjs/common: "^9.0.0", it's required to use UploadedFile.

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

}

Enter fullscreen mode Exit fullscreen mode

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

...

Enter fullscreen mode Exit fullscreen mode

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)`
  }
}

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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

...

Enter fullscreen mode Exit fullscreen mode

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

}

Enter fullscreen mode Exit fullscreen mode

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

...

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

...

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
codingwatchcollector profile image
Anton Begunenko

Thanks! I've been looking for a way to make file upload optional, but keep the validation pipe. Thank you for mentionning this, it saved my time :)