nestjs-fastify-upload
v1.0.3
Published
A high-performance, secure, and minimal fastify-multipart upload plugin for NestJS
Maintainers
Readme
nestjs-fastify-upload
A high-performance, secure, and minimal fastify-multipart upload plugin for NestJS. Engineered for scale and high concurrency, with optional in-memory processing for cloud-native workflows.
Supports ALL file types (Images, Videos, PDFs, CSVs, Binaries, etc.). Stream directly to disk or keep in memory for cloud uploads.
Features
- Extreme Performance: Low latency, streams directly to disk avoiding RAM allocations efficiently handling massive concurrent uploads.
- Memory Mode:
storage: 'memory'returns a Buffer — no disk I/O. Ideal for uploading directly to R2, S3, GDrive, etc. - Image Optimization: Built-in Sharp integration for resize, format conversion, and quality optimization.
- Provider Hook:
onFilecallback lets you wire your own cloud provider logic after file processing. - Secure by Default: Validates MIME types, file extensions, and file sizes strictly.
- Minimal Dependencies: Only
@fastify/multipartand@fastify/busboy. Sharp is optional. - Easy NestJS Integration: Custom intuitive decorators for handling
@UploadFile()and@UploadFiles(). - Form fields →
req.body: After consuming the file stream(s), non-file multipart fields are merged intoreq.bodyfor@Body().
Installation
npm install nestjs-fastify-uploadFor image optimization, also install Sharp:
npm install sharpQuick Start
Setup main.ts
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import multipart from '@fastify/multipart';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter()
);
await app.register(multipart);
await app.listen(3000);
}
bootstrap();Controller Integration
import { Controller, Post, UseInterceptors } from '@nestjs/common';
import {
FileInterceptor, FilesInterceptor,
UploadFile, UploadFiles,
UploadedFileResult
} from 'nestjs-fastify-upload';
@Controller('upload')
export class UploadController {
// Disk mode (default) — streams directly to disk
@Post('profile-picture')
@UseInterceptors(FileInterceptor('file', {
dest: './storage/images',
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
optimizeImage: { w: 400, h: 400, quality: 80 },
maxFileSize: 5 * 1024 * 1024,
}))
uploadProfilePicture(@UploadFile() file: UploadedFileResult) {
return { message: 'File saved to disk', file };
}
// Memory mode — returns Buffer, no disk writes
@Post('upload-to-cloud')
@UseInterceptors(FileInterceptor('file', {
storage: 'memory',
allowedMimeTypes: ['image/png', 'application/pdf'],
maxFileSize: 10 * 1024 * 1024,
}))
async uploadToCloud(@UploadFile() file: UploadedFileResult) {
// file.buffer is available — send directly to R2/S3/GDrive
await this.myCloudService.upload(file.buffer, file.name);
return { message: 'Uploaded to cloud', name: file.name, checksum: file.checksum };
}
// Memory mode + onFile hook — auto-send to provider
@Post('auto-upload')
@UseInterceptors(FileInterceptor('file', {
storage: 'memory',
onFile: async (file) => {
const result = await myS3Provider.upload(file);
return { url: result.url, providerResult: result };
},
}))
autoUpload(@UploadFile() file: UploadedFileResult) {
return { url: file.url, providerResult: file.providerResult };
}
// Multiple files
@Post('gallery')
@UseInterceptors(FilesInterceptor('files', {
dest: './storage/gallery',
maxFiles: 5,
maxFileSize: 10 * 1024 * 1024,
}))
uploadGallery(@UploadFiles() files: UploadedFileResult[]) {
return { message: 'Files saved!', files };
}
}Options
| Option | Type | Default | Description |
|---|---|---|---|
| dest | string | './' | Target directory (required for storage: 'disk'). Created automatically. |
| storage | 'disk' \| 'memory' | 'disk' | 'disk' streams to disk (zero RAM). 'memory' returns a Buffer. |
| maxFileSize | number | 5MB | Maximum bytes per file. |
| allowedExtensions | string[] | — | Allowed file extensions, e.g. ['.jpg', '.pdf']. |
| allowedMimeTypes | string[] | — | Allowed MIME types, e.g. ['image/png']. |
| optimizeImage | boolean \| OptimizeImageOptions | — | Optimize images with Sharp. Requires sharp to be installed. |
| ~~resizeImage~~ | { w, h } | — | Deprecated. Use optimizeImage instead. |
| maxFiles | number | 10 | Max files for @UploadFiles(). |
| onFile | (file: FilePayload) => Promise<Partial<UploadedFileResult>> | — | Post-processing hook. Only runs in storage: 'memory' mode. |
OptimizeImageOptions
| Option | Type | Default | Description |
|---|---|---|---|
| w | number | — | Target width (cover crop). |
| h | number | — | Target height (cover crop). |
| quality | number | 80 | Output quality (1–100). |
| format | 'jpeg' \| 'png' \| 'webp' \| 'avif' | Same as input | Output format. |
| withoutEnlargement | boolean | true | Don't upscale images smaller than target. |
Storage Modes
storage: 'disk' (default)
Files are streamed directly to disk using Node.js pipelines. Memory stays under 20MB even for multi-GB files. SHA-256 is computed incrementally during streaming.
When optimizeImage is enabled in disk mode, Sharp reads the temp file, optimizes it, and writes the final result — no large buffers in RAM.
storage: 'memory'
The entire file is collected into a Buffer. After processing, the Buffer is returned in UploadedFileResult.buffer. No disk writes occur.
Best for:
- Uploading directly to cloud providers (R2, S3, GDrive, etc.)
- Temporary processing where disk I/O is undesirable
- Small to medium files
Provider Integration
onFile Hook
The onFile callback receives a FilePayload after the file is fully processed and lets you merge extra data into the result.
@UseInterceptors(FileInterceptor('file', {
storage: 'memory',
onFile: async (file: FilePayload) => {
const { url, id } = await r2Provider.upload(file.buffer, file.filename);
return { url, providerResult: { id } };
},
}))UploadProvider Interface
For reusable provider logic across projects, implement the UploadProvider interface:
import { UploadProvider, FilePayload } from 'nestjs-fastify-upload';
class MyS3Provider implements UploadProvider {
readonly name = 's3';
async upload(file: FilePayload): Promise<Record<string, any>> {
const result = await s3Client.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: file.filename,
Body: file.buffer,
}));
return { url: `https://s3.amazonaws.com/${process.env.S3_BUCKET}/${file.filename}`, etag: result.ETag };
}
async delete(id: string): Promise<void> {
await s3Client.send(new DeleteObjectCommand({ Bucket: process.env.S3_BUCKET, Key: id }));
}
}Types
type UploadStorage = 'disk' | 'memory';
interface FilePayload {
buffer: Buffer;
filename: string;
mimetype: string;
ext: string;
size: number;
checksum: string;
}
interface UploadProvider {
readonly name: string;
upload(file: FilePayload): Promise<Record<string, any>>;
delete?(id: string): Promise<void>;
}
interface OptimizeImageOptions {
w?: number;
h?: number;
quality?: number;
format?: 'jpeg' | 'png' | 'webp' | 'avif';
withoutEnlargement?: boolean;
}
interface UploadOptions {
dest?: string;
storage?: UploadStorage;
maxFileSize?: number;
allowedExtensions?: string[];
allowedMimeTypes?: string[];
/** @deprecated Use optimizeImage */
resizeImage?: { w: number; h: number };
optimizeImage?: boolean | OptimizeImageOptions;
maxFiles?: number;
onFile?: (file: FilePayload) => Promise<Partial<UploadedFileResult>>;
}
interface UploadedFileResult {
name: string;
path?: string; // Set in disk mode
ext: string;
size: number;
checksum: string;
buffer?: Buffer; // Set in memory mode
url?: string; // Set by onFile
providerResult?: Record<string, any>; // Set by onFile
}Architecture & Security
- Asynchronous Disk Piping: Direct Node Stream pipelines push multipart TCP packets to disk without buffering in RAM.
- Memory Mode: Collects chunks into a Buffer — zero disk I/O. Ideal for cloud-native pipelines.
- Auto-Cleanup: Partial files from aborted connections or validation failures are safely unlinked.
- SHA-256 Verification: Every file is checksummed, whether streamed to disk or kept in memory.
Author
@royaltics.solutions
