(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…
(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-expressintegrates Multer with Nest’s interceptor system.multerhandles parsingmultipart/form-datarequests.
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:
- Enforce size and MIME-type restrictions on single and multiple uploads
- Require at least one file per field (when you need it)
- Surface clear, human-readable errors back to clients
- 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