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.
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.
- 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
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 {}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.
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.
avatar: MemoryStoredFile;The file is loaded entirely into RAM as a Buffer. Fast, but not suitable for large files.
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.
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 })
@Module({
imports: [
NestjsFormDataModule.config({
storage: MemoryStoredFile,
isGlobal: true,
limits: {
fileSize: 5e6, // 5 MB
files: 10,
},
}),
],
})
export class AppModule {}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',
};
}
}Override global config for a specific endpoint:
@Post('upload')
@FormDataRequest({ storage: FileSystemStoredFile })
upload(@Body() dto: UploadDto) {}| 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 |
All validators work with { each: true } for arrays of files.
Checks if the value is an uploaded file (instance of StoredFile).
@IsFile()
avatar: MemoryStoredFile;
@IsFiles()
photos: MemoryStoredFile[];File size constraints in bytes.
@MaxFileSize(5e6) // max 5 MB
@MinFileSize(1024) // min 1 KB
avatar: MemoryStoredFile;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.
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.
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):
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):
export class CreateProductDto {
@IsString()
name: string;
@IsNumber()
@Type(() => Number)
price: number;
@IsFile()
@MaxFileSize(5e6)
@HasMimeType(['image/*'])
image: MemoryStoredFile;
}See CHANGELOG.md.

