← Back to list

(NestJS-15) Validating Multiple File Uploads in NestJS Using Custom Pipe

Handling file uploads is a common requirement in modern web applications — but validating those uploads by type, size, and structure is…

Bhargava Chary · 2025-05-10 12:19 · 2 claps · 3.7 min read
#multiple-file-upload #file-upload #nestjs #nestjs-tutorial #multer
Open on Medium ↗
Wiki topics: 🌐 · Web Development 💑 · Relationships

(NestJS-15) Validating Multiple File Uploads in NestJS Using Custom Pipe

Handling file uploads is a common requirement in modern web applications — but validating those uploads by type, size, and structure is equally important for security, scalability, and user experience. In this guide, you’ll learn how to implement both single and multiple file uploads in NestJS using a reusable FilesValidationPipe.

Whether you’re building a CMS, an e-learning platform, or a document management system, you’ll walk away with battle-tested patterns for:

  • Validate file type and size
  • Customizable options via pipe
  • Logging user activity via IP
  • Elegant error handling for missing or invalid uploads

Setup and Required Packages

Ensure the followingFirst, install the NestJS file-upload middleware:

npm install @nestjs/platform-express multer
  • @nestjs/platform-express integrates Multer with Nest’s interceptor system.
  • multer handles parsing multipart/form-data requests.

Creating the FileValidationOptions Interface

Define a flexible interface for your pipe’s configuration:

// src/common/pipes/file-validation.interface.ts
import { PipeTransform, Injectable, BadRequestException, ArgumentMetadata } from '@nestjs/common';

export interface FileValidationOptions {
  /** Maximum allowed file size in bytes (default: 1 MB) */
  maxSize?: number;
  /** Allowed MIME types (default: ['application/pdf']) */
  allowedTypes?: string[];
  /** Require at least one file per field? (default: true) */
  requireFilesInEachField?: boolean;
  /** Custom storage path (for future use) */
  store?: string;
}

Implementing the FilesValidationPipe

Create the pipe class that merges user options with sensible defaults:

// src/common/pipes/multiple-files-validation.pipe.ts
@Injectable()
export class FilesValidationPipe implements PipeTransform {
  private readonly options: Required<FileValidationOptions>;

  constructor(options: FileValidationOptions = {}) {
    this.options = {
      maxSize: options.maxSize ?? 1 * 1024 * 1024,
      allowedTypes: options.allowedTypes ?? ['application/pdf'],
      requireFilesInEachField: options.requireFilesInEachField ?? true,
      store: options.store ?? '/upload',
    };
  }

  transform(
    value: Express.Multer.File | Express.Multer.File[] | Record<string, Express.Multer.File[]>,
    metadata: ArgumentMetadata,
  ): any {

    if (!value) {
      throw new BadRequestException('No files uploaded');
    }

    // 1️⃣ Single-file
    if (this.isSingleFile(value)) {
      this.validateSingleFile(value);
      return value;
    }

    // 2️⃣ Array of files for one field (FilesInterceptor)
    if (Array.isArray(value)) {
      if (this.options.requireFilesInEachField && value.length === 0) {
        throw new BadRequestException(`At least one file is required`);
      }
      // validate each as a single file
      value.forEach((file) => {
        if (!this.isSingleFile(file)) {
          throw new BadRequestException('Invalid file in array');
        }
        this.validateSingleFile(file);
      });
      return value;
    }

    // 3️⃣ Multiple fields (FileFieldsInterceptor)
    if (this.isMultipleFilesMap(value)) {
      // require each field to have at least one file?
      if (this.options.requireFilesInEachField) {
        for (const [field, arr] of Object.entries(value)) {
          if (!Array.isArray(arr) || arr.length === 0) {
            throw new BadRequestException(`Field "${field}" requires at least one file`);
          }
        }
      }
      // validate all
      for (const arr of Object.values(value)) {
        arr.forEach(file => this.validateSingleFile(file, arr));
      }
      return value;
    }

    throw new BadRequestException('Invalid file upload format');
  }

  // …helper methods below…
}

Type Guards

Distinguish between single-file and multi-file payloads:

  private isSingleFile(value: any): value is Express.Multer.File {
    return (
      value &&
      typeof value === 'object' &&
      'fieldname' in value &&
      'originalname' in value &&
      'mimetype' in value &&
      'size' in value
    );
  }

  private isMultipleFilesMap(
    value: any,
  ): value is Record<string, Express.Multer.File[]> {
    return (
      typeof value === 'object' &&
      value !== null &&
      !Array.isArray(value) &&
      Object.values(value).every(
        arr => Array.isArray(arr) && arr.every(f => this.isSingleFile(f)),
      )
    );
  }

Validating a Single File

Check file size and MIME type; throw a readable error if invalid:

 private validateSingleFile(
    file: Express.Multer.File,
    _arr?: Express.Multer.File[],
  ) {
    const { maxSize, allowedTypes } = this.options;

    // choose KB vs MB
    let limitLabel: string;
    if (maxSize < 1024 * 1024) {
      const kb = Math.round(maxSize / 1024);
      limitLabel = `${kb} KB`;
    } else {
      const mb = Math.round((maxSize / 1024 / 1024) * 10) / 10;
      limitLabel = `${mb} MB`;
    }

    if (file.size > maxSize) {
      throw new BadRequestException(
        `"${file.originalname}" exceeds ${limitLabel}`,
      );
    }

    if (!allowedTypes.includes(file.mimetype)) {
      const types = allowedTypes.map(t => this.getTypeName(t)).join(', ');
      throw new BadRequestException(
        `"${file.originalname}" must be one of: ${types}`,
      );
    }
  }

Human-Readable MIME Types

Convert MIME strings into friendly labels for error messages:

private getTypeName(mime: string): string {
  const map: Record<string, string> = {
    'application/pdf': 'PDF',
    'image/jpeg': 'JPEG',
    'image/png': 'PNG',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'DOCX',
  };
  return map[mime] || mime;
}

This helper makes error messages more human-readable: *File must be: PDF, JPEG instead of MIME gibberish*

Integrating the Pipe in Controllers

Single-File Upload

  @Post('single')
  @UseInterceptors(FileInterceptor('file'))
  uploadSingle(
    @Req() req: any,
    @UploadedFile(
      new FilesValidationPipe({
        maxSize: 500 * 1024,
        allowedTypes: ['image/jpeg'],
      }),
    )
    file: Express.Multer.File,
  ) {
    return this.appService.handleSingle(file);
  }

Multiple files on one Field

  @Post('many')
  @UseInterceptors(FilesInterceptor('files', 5))  // up to 5 files under “files”
  uploadMany(
    @Req() req: any,
    @UploadedFiles(
      new FilesValidationPipe({
        maxSize: 2 * 1024 * 1024,
        allowedTypes: ['image/png', 'image/jpeg'],
      }),
    )
    files: Express.Multer.File[],
  ) {
    return this.appService.manyFiles(files);
  }

Multiple Fields with Multiple Files

@Post('mixed')
  @UseInterceptors(
    FileFieldsInterceptor([
      { name: 'documents', maxCount: 3 },
      { name: 'images', maxCount: 5 },
    ]),
  )
  uploadMixed(
    @Req() req: any,
    @UploadedFiles(
      new FilesValidationPipe({
        maxSize: 10 * 1024 * 1024,
        allowedTypes: ['application/pdf', 'image/jpeg', 'image/png'],
        requireFilesInEachField: true,
      }),
    )
    files: Record<string, Express.Multer.File[]>,
  ) {
    return this.appService.mixedFiles(files);
  }

Conclusion

You now have a battle-tested recipe for handling file uploads in NestJS with full validation and error handling. By building a reusable FilesValidationPipe, you can:

  1. Enforce size and MIME-type restrictions on single and multiple uploads
  2. Require at least one file per field (when you need it)
  3. Surface clear, human-readable errors back to clients
  4. Keep controllers slim by offloading validation logic to a pipe

Next Steps

In our next article, we will explore Background Jobs in NestJS — Implementing scheduled tasks. You’ll learn how to run recurring jobs, schedule one-off tasks, and integrate popular libraries like Bull or Agenda seamlessly into your NestJS apps.

GitHub Repository Find the complete code on GitHub: bhargavachary123

If you found this article helpful, leave a clap (👏) and a comment! 🚀


메타데이터
post_id
ce75889c9768
slug
nestjs-15-validating-multiple-file-uploads-in-nestjs-using-custom-pipe-ce75889c9768
url
https://medium.com/@bhargavacharyb/nestjs-15-validating-multiple-file-uploads-in-nestjs-using-custom-pipe-ce75889c9768
canonical_url
https://medium.com/@bhargavacharyb/nestjs-15-validating-multiple-file-uploads-in-nestjs-using-custom-pipe-ce75889c9768
author_url
https://medium.com/@bhargavacharyb
status
ok
fetched_at
2026-09-08 15:54:05