nestjs-form-data
v11.0.1
Published
NestJS middleware for handling multipart/form-data, which is primarily used for uploading files
Maintainers
Readme
nestjs-form-data
An object-oriented approach to handling multipart/form-data in NestJS. Uploaded files become typed class instances with built-in validation, automatic cleanup, and pluggable storage — no manual stream wiring required.
Why nestjs-form-data?
NestJS ships with Multer-based file upload, but it works outside the DTO validation flow — you handle files through @UploadedFile() separately from @Body(), losing the single-source-of-truth that DTOs provide.
nestjs-form-data takes a different approach: files are first-class properties on your DTO. They arrive as typed objects (MemoryStoredFile, FileSystemStoredFile, or your own custom class), validated with the same decorators you already use for strings and numbers:
export class CreatePostDto {
@IsString()
title: string;
@IsFile()
@MaxFileSize(5e6)
@HasMimeType(['image/jpeg', 'image/png'])
cover: MemoryStoredFile;
}No @UploadedFile(), no separate pipes, no manual cleanup. Just a DTO.
Key features
- Files as typed objects — each uploaded file is an instance of a
StoredFileclass with properties likesize,mimeType,extension,originalName, and a reliablebufferorpath - Declarative validation — validate file size, MIME type, and extension with decorators, including support for arrays (
{ each: true }) - Reliable MIME detection — uses file-type to read the file's magic number, falling back to the content-type header only when needed
- Nested objects — fields with bracket notation (
photos[0][name]) are parsed into proper nested structures - Pluggable storage — choose
MemoryStoredFilefor speed,FileSystemStoredFilefor large files, or extendStoredFileto write your own (S3, GCS, etc.) - Automatic cleanup — temporary files are deleted after the request completes (configurable per success/failure)
- Express and Fastify support
- NestJS 7 – 11 compatible
Installation
npm install nestjs-form-dataThis module requires class-validator and class-transformer as peer dependencies:
npm install class-validator class-transformerRegister a global validation pipe in main.ts:
import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes(
new ValidationPipe({
transform: true, // recommended to avoid issues with file array transformations
}),
);Add the module to your application:
import { NestjsFormDataModule } from 'nestjs-form-data';
@Module({
imports: [NestjsFormDataModule],
})
export class AppModule {}Quick start
Apply @FormDataRequest() to your controller method and define a DTO:
import { Controller, Post, Body } from '@nestjs/common';
import { FormDataRequest, MemoryStoredFile, IsFile, MaxFileSize, HasMimeType } from 'nestjs-form-data';
class UploadAvatarDto {
@IsFile()
@MaxFileSize(1e6)
@HasMimeType(['image/jpeg', 'image/png'])
avatar: MemoryStoredFile;
}
@Controller('users')
export class UsersController {
@Post('avatar')
@FormDataRequest()
uploadAvatar(@Body() dto: UploadAvatarDto) {
// dto.avatar is a MemoryStoredFile instance
console.log(dto.avatar.originalName); // "photo.jpg"
console.log(dto.avatar.size); // 94521
console.log(dto.avatar.mimeType); // "image/jpeg"
console.log(dto.avatar.buffer); // <Buffer ff d8 ff ...>
}
}That's it. The file is parsed, validated, and available as a typed object on your DTO.
Fastify
Install @fastify/multipart and register it:
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import multipart from '@fastify/multipart';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());
app.register(multipart);
await app.listen(3000);
}Everything else works the same — DTOs, decorators, and storage types are platform-agnostic.
File storage types
Memory storage
avatar: MemoryStoredFile;The file is loaded entirely into RAM as a Buffer. Fast, but not suitable for large files.
File system storage
avatar: FileSystemStoredFile;The file is written to a temporary directory on disk and available via file.path during request processing. Automatically deleted when the request completes.
Custom storage
Extend the StoredFile abstract class to implement your own storage (e.g., stream directly to S3):
import { StoredFile } from 'nestjs-form-data';
export class S3StoredFile extends StoredFile {
s3Key: string;
size: number;
static async create(meta, stream, config): Promise<S3StoredFile> {
// upload stream to S3, return instance
}
async delete(): Promise<void> {
// delete from S3
}
}Then use it: @FormDataRequest({ storage: S3StoredFile })
Configuration
Static
@Module({
imports: [
NestjsFormDataModule.config({
storage: MemoryStoredFile,
isGlobal: true,
limits: {
fileSize: 5e6, // 5 MB
files: 10,
},
}),
],
})
export class AppModule {}Async
NestjsFormDataModule.configAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
storage: MemoryStoredFile,
limits: {
files: configService.get<number>('MAX_FILES'),
},
}),
inject: [ConfigService],
});You can also use useClass or useExisting patterns — see NestJS async providers for details:
// useClass — creates a new instance
NestjsFormDataModule.configAsync({
useClass: MyFormDataConfigService,
});
// useExisting — reuses an imported provider
NestjsFormDataModule.configAsync({
imports: [MyConfigModule],
useExisting: MyFormDataConfigService,
});Where the config service implements:
export class MyFormDataConfigService implements NestjsFormDataConfigFactory {
configAsync(): FormDataInterceptorConfig {
return {
storage: FileSystemStoredFile,
fileSystemStoragePath: '/tmp/nestjs-fd',
};
}
}Method-level override
Override global config for a specific endpoint:
@Post('upload')
@FormDataRequest({ storage: FileSystemStoredFile })
upload(@Body() dto: UploadDto) {}Configuration options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| storage | Type<StoredFile> | MemoryStoredFile | Storage class for uploaded files |
| isGlobal | boolean | false | Make the module available to all submodules |
| fileSystemStoragePath | string | /tmp/nestjs-tmp-storage | Temp directory for FileSystemStoredFile |
| cleanupAfterSuccessHandle | boolean | true | Delete files after successful request |
| cleanupAfterFailedHandle | boolean | true | Delete files after failed request |
| awaitCleanup | boolean | true | Wait for file cleanup before sending response. Set to false for faster responses (cleanup runs in the background) |
| limits | object | {} | Busboy limits: fileSize, files, fields, parts, headerPairs |
Validation decorators
All validators work with { each: true } for arrays of files.
@IsFile / @IsFiles
Checks if the value is an uploaded file (instance of StoredFile).
@IsFile()
avatar: MemoryStoredFile;
@IsFiles()
photos: MemoryStoredFile[];@MaxFileSize / @MinFileSize
File size constraints in bytes.
@MaxFileSize(5e6) // max 5 MB
@MinFileSize(1024) // min 1 KB
avatar: MemoryStoredFile;@HasMimeType
Validate MIME type. Supports exact strings, wildcard patterns, and regular expressions.
// exact match
@HasMimeType(['image/jpeg', 'image/png'])
// wildcard — matches any image type
@HasMimeType('image/*')
// regex
@HasMimeType([/image\/.*/])MIME type detection priority:
- Magic number (via file-type) — reads binary data, reliable
- Content-Type header (via busboy) — client-provided, can be spoofed
To enforce that the MIME type comes from a specific source:
import { MetaSource } from 'nestjs-form-data';
@HasMimeType(['image/jpeg'], MetaSource.bufferMagicNumber) // only trust magic numberAccess the source at runtime via file.mimeTypeWithSource.
@HasExtension
Validate file extension. Same source priority and strict mode as @HasMimeType.
@HasExtension(['jpg', 'png'])
// strict — only trust extension from magic number detection
@HasExtension(['jpg'], MetaSource.bufferMagicNumber)Access the source at runtime via file.extensionWithSource.
Examples
Single file with FileSystemStoredFile
Controller:
import { FileSystemStoredFile, FormDataRequest } from 'nestjs-form-data';
@Controller()
export class NestjsFormDataController {
@Post('load')
@FormDataRequest({ storage: FileSystemStoredFile })
getHello(@Body() testDto: FormDataTestDto): void {
console.log(testDto);
}
}DTO:
import { FileSystemStoredFile, HasMimeType, IsFile, MaxFileSize } from 'nestjs-form-data';
export class FormDataTestDto {
@IsFile()
@MaxFileSize(1e6)
@HasMimeType(['image/jpeg', 'image/png'])
avatar: FileSystemStoredFile;
}Send request (via Insomnia):

Array of files
DTO:
import { FileSystemStoredFile, HasMimeType, IsFiles, MaxFileSize } from 'nestjs-form-data';
export class FormDataTestDto {
@IsFiles()
@MaxFileSize(1e6, { each: true })
@HasMimeType(['image/jpeg', 'image/png'], { each: true })
avatars: FileSystemStoredFile[];
}Send request (via Insomnia):

Mixed fields and files
export class CreateProductDto {
@IsString()
name: string;
@IsNumber()
@Type(() => Number)
price: number;
@IsFile()
@MaxFileSize(5e6)
@HasMimeType(['image/*'])
image: MemoryStoredFile;
}Changelog
See CHANGELOG.md.
