In NestJS 10, how do I set my FileValidator to only accept image uploads?

783 Views Asked by At

I'm using NestJS 10. I want to create a file upload endpoint that only accepts image file uploads. I created

  @Post()
  @UseInterceptors(
    FileFieldsInterceptor([
      { name: 'frontImg', maxCount: 1 },
      { name: 'backImg', maxCount: 1 },
    ]),
  )
  async create(
    @Req() req: Request,
    @Body('title') title: string,
    @Body('frontImgAltText') frontImgAltText: string,
    @Body('category') category: Occasion,
    @UploadedFiles(
      new ParseFilePipe({
        validators: [
          new FileTypeValidator({ fileType: /(jpg|jpeg|png|webp)$/ }),
        ],
      }),
    )
    files: { frontImg: Express.Multer.File[]; backImg?: Express.Multer.File[] },
  ) { ... }

but when I execute a request against the endpoint, it consistently fails with the error

{
    "message": "Validation failed (expected type is /(jpg|jpeg|png|webp)$/)",
    "error": "Bad Request",
    "statusCode": 400
}

What's the proper way to create a validator that only accepts certain image file types?

4

There are 4 best solutions below

0
On BEST ANSWER

The issue here is that when isValid method from the built-in FileTypeValidator is called it gets an object with the two fields frontImg and backImg: enter image description here and it makes sense since files is defined like that here:

 files: { frontImg: Express.Multer.File[]; backImg?: Express.Multer.File[] }

and it doesn't have any mimetype whatsoever.

I think one way to work around this can be to create a small pipe that forwards every file to FileTypeValidator like this:

    @UseInterceptors(
    FileFieldsInterceptor([
      { name: 'frontImg', maxCount: 1 },
      { name: 'backImg', maxCount: 1 },
    ]),
  )
  async uploadTest(
    @UploadedFiles({
      transform: (fileRequest: {
        frontImg: Express.Multer.File[];
        backImg?: Express.Multer.File[];
      }) => {
        const validator = new FileTypeValidator({
          fileType: /(jpg|jpeg|png|webp)$/,
        });
        if (
          validator.isValid(fileRequest.frontImg[0]) &&
          validator.isValid(fileRequest.backImg[0])
        ) {
          return fileRequest;
        }

        throw new HttpException(validator.buildErrorMessage(), 400);
      },
    })
    files: {
      frontImg: Express.Multer.File[];
      backImg?: Express.Multer.File[];
    },
  ) {
    console.log(files);
  }
2
On

You might consider, instead of using FileTypeValidator, to create a custom file validation function or class, and validate the mimetype of the uploaded files (similar to Nest / File Upload / File validation).

import { PipeTransform, BadRequestException } from '@nestjs/common';

export class ImageFileValidationPipe implements PipeTransform {
  transform(files: Express.Multer.File[]): Express.Multer.File[] {
    const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
    for (let file of files) {
      if (!allowedMimeTypes.includes(file.mimetype)) {
        throw new BadRequestException('Invalid file type');
      }
    }
    return files;
  }
}

And then update your create handler to use this custom ImageFileValidationPipe:

@Post()
@UseInterceptors(
  FileFieldsInterceptor([
    { name: 'frontImg', maxCount: 1 },
    { name: 'backImg', maxCount: 1 },
  ]),
)
async create(
  @Req() req: Request,
  @Body('title') title: string,
  @Body('frontImgAltText') frontImgAltText: string,
  @Body('category') category: Occasion,
  @UploadedFiles() files: { frontImg: Express.Multer.File[]; backImg?: Express.Multer.File[] },
) {
  const validatedFiles = new ImageFileValidationPipe().transform([...files.frontImg, ...(files.backImg || [])]);
  // rest of your code
}

New flow:

+--------+      +------------------+      +-----------------+      +------------------+
| Client | ---> | NestJS 10        | ---> | FileFields      | ---> | create() Handler |
|        |      | Endpoint         |      | Interceptor     |      | in Controller    |
+--------+      +------------------+      +-----------------+      +------------------+
                                                    |                    |
                                                    |                    |
                                                    v                    v
                                         +---------------------+  +----------------------+
                                         | ImageFileValidation |  | Validated Image File |
                                         | Pipe Validation     |  | Processing           |
                                         +---------------------+  +----------------------+
2
On

In my project I created my own pipes. Below the code that you reuse for your project:

import { FileValidator, Injectable } from '@nestjs/common';
    
import fileType from 'file-type';
    
import { MIME_TYPES, MIME_TYPES_ARRAY } from '../constants';
import { IFileTypeOptions } from '../types';
    
    
@Injectable()
export class FileTypePipe extends FileValidator<IFileTypeOptions> {
  protected readonly validationOptions: { acceptableTypes: MIME_TYPES[] } = {
    acceptableTypes: [ MIME_TYPES.JPG, MIME_TYPES.PNG, MIME_TYPES.GIF ],
  };
    
  constructor(options: IFileTypeOptions) {
    super(options);
    
    if (!options || (!Array.isArray(options.acceptableTypes) && typeof options.acceptableTypes !== 'string')) {
      throw new Error('Format of MIME types passed to File Type Validator is unreadable');
    }
    
    const acceptableTypes = Array.isArray(options.acceptableTypes)
      ? options.acceptableTypes
      : [options.acceptableTypes];
    
    for (let i = 0; i < acceptableTypes.length; i++) {
      if (!MIME_TYPES_ARRAY.includes(acceptableTypes[i])) {
        throw new Error('Unknown MIME type passed to File Type Validator');
      }
    }
    
    this.validationOptions.acceptableTypes = acceptableTypes;
  }
    
  public buildErrorMessage(file: Express.Multer.File): string {
    return `Actual file '${file.originalname}' has unacceptable MIME type. List of acceptable types: ${this.validationOptions.acceptableTypes.join(', ')}.`;
  }
    
  public async isValid(file?: Express.Multer.File): Promise<boolean> {
    const actualMimeType = fileType(file.buffer)?.mime;
    
    return this.validationOptions.acceptableTypes.includes(actualMimeType as unknown as MIME_TYPES);
  }
}

Typing details file-type-options.interface.ts:

import { MIME_TYPES } from '../constants';

    
export interface IFileTypeOptions {
  acceptableTypes: MIME_TYPES | MIME_TYPES[];
}

mime-types.enum.ts:

export enum MIME_TYPES {
  JPG = 'image/jpeg',
  PNG = 'image/png',
  GIF = 'image/gif',
}
    
export const MIME_TYPES_ARRAY = Object.values(MIME_TYPES);

And using:

import {
  // ...
  ParseFilePipe,
  UploadedFile,
  // ...
} from '@nestjs/common';
      
  // ...
      
  @UpdateVendor()
  @ApiConsumes('multipart/form-data')
  @UseInterceptors(FileInterceptor('avatar'))
  @UseGuards(JustMeGuard)
  @Patch('me/:_id')
  public async updateMe(
    @Param() param: UpdateVendorParamDto,
    @Body() body: UpdateVendorBodyDto,
    @Req() req: AuthenticatedRequest,
    @UploadedFile(
      new ParseFilePipe({
        validators: [
          new FileSizePipe({ maxFileSize: AVATAR_FILE_BYTE_SIZE }),
          // USAGE EXAMPLE
          new FileTypePipe({ acceptableTypes: [ MIME_TYPES.JPG, MIME_TYPES.PNG, MIME_TYPES.GIF ] }),
        ],
        fileIsRequired: false,
      }),
    ) avatar?: Express.Multer.File,
  ): Promise<IResponse<IUpdateVendorRes | IImageSavingInited>> {
    // my endpoint code goes there
  }
0
On

I've created a custom interceptor that extends the functionality of the built-in FileInterceptor and FilesInterceptor. This interceptor is designed to allow only media file uploads (images and videos) and can be configured for both single and multiple file uploads.

Usage:

@Post()
@UseInterceptors(new MediaFilesInterceptor({
  fieldName: 'covers',
  errorMessage: 'Invalid file type: Only image and video files are allowed',
  singleFile: false // or true for single file upload
}))
async uploadFile(@UploadedFiles() files) {
  // Handle your file upload logic here
}

interceptor

import { Injectable, NestInterceptor, ExecutionContext, CallHandler, UnsupportedMediaTypeException, } from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { Observable } from 'rxjs';

 
interface CustomFilesInterceptorOptions {
  fieldName: string;
  errorMessage: string;
  singleFile?: boolean;
}

@Injectable()
export class MediaFilesInterceptor implements NestInterceptor {
  private interceptor;

  constructor(private options: CustomFilesInterceptorOptions) {
    const fileFilter = (
      req: any,
      file: Express.Multer.File,
      callback: (error: Error | null, acceptFile: boolean) => void,
    ) => {
      if (
        !file.mimetype.startsWith('image/') &&
        !file.mimetype.startsWith('video/')
      ) {
        return callback(
          new UnsupportedMediaTypeException(this.options.errorMessage),
          false,
        );
      }
      callback(null, true);
    };

    this.interceptor = options.singleFile
      ? new (FileInterceptor(options.fieldName, { fileFilter }))()
      : new (FilesInterceptor(options.fieldName, undefined, { fileFilter }))();
  }

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return this.interceptor.intercept(context, next);
  }
}