npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nestjs-dynamic-uploader

v0.1.0

Published

A reusable NestJS local file uploader module with dynamic upload interceptors, validation, normalized responses, and optional Sharp image processing.

Downloads

162

Readme

NestJS Dynamic Uploader

A reusable NestJS local file uploader module built on top of Multer. It provides dynamic upload interceptors, folder-based local storage, file validation, normalized file responses, custom validator/processor hooks, and optional Sharp-based image processing.

Package name used in this template: nestjs-dynamic-uploader
Rename the name field in package.json before publishing if you want a scoped or project-specific package name.

Features

  • Single file upload interceptor
  • Multiple files upload interceptor
  • Multiple named file fields upload interceptor
  • Any files upload interceptor for dynamic/unknown fields
  • Automatic local folder creation under public/uploads/{folder}
  • Safe folder-name sanitization
  • Generated collision-resistant filenames
  • MIME type and extension validation
  • Required-file validation
  • Custom post-upload validators
  • Custom post-upload processors
  • Optional Sharp image resizing/compression
  • Optional thumbnail generation
  • Normalized file response helper
  • Structured upload error responses

Installation

Install the package in a NestJS application:

npm install nestjs-dynamic-uploader

This package expects these NestJS peer dependencies to already exist in your application:

npm install @nestjs/common @nestjs/platform-express rxjs

multer and sharp are included as runtime dependencies because the uploader stores files using Multer and the image processor imports Sharp.

Build from source

When working on this package locally:

npm install
npm run build

The compiled package output is generated in:

dist/

To create a local npm tarball:

npm pack

To install the tarball in another project:

npm install /path/to/nestjs-dynamic-uploader-0.1.0.tgz

Basic setup

Import UploaderModule in the module where you want to use upload utilities.

import {Module} from '@nestjs/common';
import {UploaderModule} from 'nestjs-dynamic-uploader';
import {UsersController} from './users.controller';

@Module({
    imports: [UploaderModule],
    controllers: [UsersController],
})
export class UsersModule {
}

Serving uploaded files

This uploader saves files under:

process.cwd()/public/uploads/{folder}

For example, if folder is profile, files are stored in:

public/uploads/profile

The normalized response contains public URLs such as:

/profile/profile-20260430153022-7d9f8a2c.png

Your NestJS application must serve the public directory separately. One common setup is @nestjs/serve-static:

npm install @nestjs/serve-static
import {Module} from '@nestjs/common';
import {ServeStaticModule} from '@nestjs/serve-static';
import {join} from 'path';

@Module({
    imports: [
        ServeStaticModule.forRoot({
            rootPath: join(process.cwd(), 'public', 'uploads'),
            serveRoot: '/',
        }),
    ],
})
export class AppModule {
}

Single file upload

Use DynamicFileInterceptor when a route accepts one file.

import {
    Controller,
    Post,
    UploadedFile,
    UseInterceptors,
} from '@nestjs/common';
import {
    DynamicFileInterceptor,
    UploaderService,
} from 'nestjs-dynamic-uploader';

@Controller('users')
export class UsersController {
    constructor(private readonly uploaderService: UploaderService) {
    }

    @Post('profile-photo')
    @UseInterceptors(
        DynamicFileInterceptor({
            fieldName: 'profilePhoto',
            folder: 'profile',
            required: true,
            maxFileSize: 2 * 1024 * 1024,
            allowedMimeTypes: ['image/jpeg', 'image/png'],
            allowedExtensions: ['jpg', 'jpeg', 'png'],
        }),
    )
    uploadProfilePhoto(@UploadedFile() file: Express.Multer.File) {
        return this.uploaderService.normalizeFile(file, 'profile');
    }
}

Multiple files under one field

Use DynamicFilesInterceptor when a route accepts many files under the same multipart field.

import {
    Controller,
    Post,
    UploadedFiles,
    UseInterceptors,
} from '@nestjs/common';
import {
    DynamicFilesInterceptor,
    UploaderService,
} from 'nestjs-dynamic-uploader';

@Controller('products')
export class ProductsController {
    constructor(private readonly uploaderService: UploaderService) {
    }

    @Post('gallery')
    @UseInterceptors(
        DynamicFilesInterceptor({
            fieldName: 'photos',
            folder: 'products/gallery',
            required: true,
            maxCount: 5,
            maxFileSize: 5 * 1024 * 1024,
            allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
            allowedExtensions: ['jpg', 'jpeg', 'png', 'webp'],
        }),
    )
    uploadGallery(@UploadedFiles() files: Express.Multer.File[]) {
        return files.map((file) =>
            this.uploaderService.normalizeFile(file, 'products/gallery'),
        );
    }
}

Multiple named file fields

Use DynamicFileFieldsInterceptor when a route accepts different named file fields with different folders or validation rules.

import {
    Controller,
    Post,
    UploadedFiles,
    UseInterceptors,
} from '@nestjs/common';
import {
    DynamicFileFieldsInterceptor,
    UploaderService,
} from 'nestjs-dynamic-uploader';

@Controller('users')
export class UsersController {
    constructor(private readonly uploaderService: UploaderService) {
    }

    @Post('images')
    @UseInterceptors(
        DynamicFileFieldsInterceptor({
            fields: [
                {
                    name: 'profilePhoto',
                    folder: 'profile',
                    maxCount: 1,
                    required: true,
                    allowedMimeTypes: ['image/jpeg', 'image/png'],
                    allowedExtensions: ['jpg', 'jpeg', 'png'],
                },
                {
                    name: 'coverPhoto',
                    folder: 'cover',
                    maxCount: 1,
                    allowedMimeTypes: ['image/jpeg', 'image/png'],
                    allowedExtensions: ['jpg', 'jpeg', 'png'],
                },
            ],
        }),
    )
    uploadImages(
        @UploadedFiles()
        files: {
            profilePhoto?: Express.Multer.File[];
            coverPhoto?: Express.Multer.File[];
        },
    ) {
        return {
            profilePhoto: files.profilePhoto?.map((file) =>
                this.uploaderService.normalizeFile(file, 'profile'),
            ),
            coverPhoto: files.coverPhoto?.map((file) =>
                this.uploaderService.normalizeFile(file, 'cover'),
            ),
        };
    }
}

fields must contain at least one field definition.

Any files upload

Use DynamicAnyFilesInterceptor only when your route must accept unknown or dynamic file field names.

import {
    Controller,
    Post,
    UploadedFiles,
    UseInterceptors,
} from '@nestjs/common';
import {
    DynamicAnyFilesInterceptor,
    UploaderService,
} from 'nestjs-dynamic-uploader';

@Controller('uploads')
export class UploadsController {
    constructor(private readonly uploaderService: UploaderService) {
    }

    @Post('any')
    @UseInterceptors(
        DynamicAnyFilesInterceptor({
            folder: 'misc',
            maxCount: 20,
            maxFileSize: 10 * 1024 * 1024,
        }),
    )
    uploadAny(@UploadedFiles() files: Express.Multer.File[]) {
        return files.map((file) => this.uploaderService.normalizeFile(file, 'misc'));
    }
}

For known fields, prefer DynamicFileInterceptor, DynamicFilesInterceptor, or DynamicFileFieldsInterceptor.

Upload options

Most interceptors accept these base options:

| Option | Type | Description | |---------------------|--------------------------|--------------------------------------------------------------------------| | folder | string | Folder inside public/uploads. Defaults to misc. | | maxFileSize | number | Maximum file size in bytes. Defaults to 10 MB. | | allowedMimeTypes | string[] | Allowed MIME types. If omitted, all MIME types are accepted. | | allowedExtensions | string[] | Allowed extensions without dot. If omitted, all extensions are accepted. | | required | boolean | Whether at least one file must be provided. | | validators | FileValidator[] | Custom validators that run after upload. | | processor | FilePostProcessor | Custom processor that runs after upload. | | imageProcessing | ImageProcessingOptions | Built-in Sharp image processing options. |

Extension values should be lowercase and should not include the dot:

allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf'];

Do not use:

allowedExtensions: ['.jpg', '.png', 'PDF'];

Normalized file response

UploaderService.normalizeFile(file, folder) converts a raw Multer file into a cleaner object:

{
    originalName: 'avatar.png',
        filename
:
    'profile-20260430153022-7d9f8a2c.png',
        mimetype
:
    'image/png',
        extension
:
    'png',
        size
:
    102400,
        folder
:
    'profile',
        path
:
    '/absolute/path/to/public/uploads/profile/profile-20260430153022-7d9f8a2c.png',
        url
:
    '/profile/profile-20260430153022-7d9f8a2c.png',
        fieldName
:
    'profilePhoto',
        storage
:
    'local',
        key
:
    'profile/profile-20260430153022-7d9f8a2c.png'
}

Image processing

Enable built-in Sharp processing with imageProcessing.enabled.

@UseInterceptors(
    DynamicFileInterceptor({
        fieldName: 'photo',
        folder: 'photos',
        allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
        allowedExtensions: ['jpg', 'jpeg', 'png', 'webp'],
        imageProcessing: {
            enabled: true,
            defaultWidth: 1920,
            defaultHeight: 1270,
            defaultQuality: 85,
            allowThumbnail: true,
            defaultThumbWidth: 300,
            defaultThumbHeight: 300,
            defaultThumbQuality: 80,
        },
    }),
)

Supported processed image extensions:

jpg, jpeg, png, webp, avif

SVG files are skipped by the Sharp processor even if a route allows SVG uploads.

Runtime image query parameters

The processor reads optional query parameters from the request:

| Query parameter | Description | Max | |-----------------|-------------------------------------------|--------| | width | Main image max width | 4096 | | height | Main image max height | 4096 | | quality | Main image quality | 100 | | thumbnail | true, false, 1, 0, yes, or no | — | | thumbWidth | Thumbnail width | 1024 | | thumbHeight | Thumbnail height | 1024 | | thumbQuality | Thumbnail quality | 100 |

Example request:

POST /users/profile-photo?width=800&height=800&quality=80&thumbnail=true

The main image is resized using fit: 'inside' and withoutEnlargement: true. Thumbnails are resized using fit: 'cover'.

Thumbnail metadata

If thumbnail generation is enabled and the request uses thumbnail=true, the thumbnail is saved beside the uploaded file with the prefix thumb_.

const fileInfo = this.uploaderService.normalizeFile(file, 'profile');
const thumbnail = this.uploaderService.getThumbnailInfo(file, 'profile');

return {
    ...fileInfo,
    thumbnail,
};

Example thumbnail response:

{
    filename: 'thumb_profile-20260430153022-7d9f8a2c.png',
        path
:
    '/absolute/path/to/public/uploads/profile/thumb_profile-20260430153022-7d9f8a2c.png',
        url
:
    '/profile/thumb_profile-20260430153022-7d9f8a2c.png',
        storage
:
    'local',
        key
:
    'profile/thumb_profile-20260430153022-7d9f8a2c.png'
}

If no thumbnail exists, getThumbnailInfo() returns null.

Custom validators

Custom validators run after Multer saves the file. If a validator fails, the uploaded file is deleted and a structured 422 response is thrown.

import {FileValidator} from 'nestjs-dynamic-uploader';

class MinimumFileSizeValidator implements FileValidator {
    message = 'File must be at least 1 KB.';

    isValid(file: Express.Multer.File): boolean {
        return file.size >= 1024;
    }
}

Usage:

@UseInterceptors(
    DynamicFileInterceptor({
        fieldName: 'document',
        folder: 'documents',
        validators: [new MinimumFileSizeValidator()],
    }),
)

Custom processors

Custom processors run after validators. They can modify, replace, scan, compress, or enrich uploaded files.

import {
    FilePostProcessor,
    FileProcessingContext,
} from 'nestjs-dynamic-uploader';

class AuditProcessor implements FilePostProcessor {
    async process(
        file: Express.Multer.File,
        context: FileProcessingContext,
    ): Promise<Express.Multer.File> {
        console.log('Uploaded file:', file.filename, context.folder);
        return file;
    }
}

Usage:

@UseInterceptors(
    DynamicFileInterceptor({
        fieldName: 'document',
        folder: 'documents',
        processor: new AuditProcessor(),
    }),
)

Error response format

Upload errors use a structured response shape:

{
  "statusCode": 422,
  "error": "unprocessable entity",
  "message": "Validation failed",
  "errors": [
    {
      "field": "profilePhoto",
      "messages": [
        "profilePhoto file is required."
      ]
    }
  ]
}

Common error scenarios include:

  • Required file missing
  • Invalid MIME type
  • Invalid file extension
  • File too large
  • Too many files
  • Unexpected file field
  • Image processing parameter validation failure
  • Custom validator failure

Folder rules

The uploader rejects unsafe folder paths to prevent path traversal.

Allowed examples:

profile
user/profile
product-images
documents/2026

Rejected examples:

../private
/etc/passwd
profile/../../../secret
profile images

Folder names can contain letters, numbers, slash, underscore, and hyphen.

Defaults

| Setting | Default | |----------------------------------|--------------------------| | Base public directory | public/uploads | | Default upload folder | misc | | Default max file size | 10 * 1024 * 1024 bytes | | Multiple files default max count | 10 | | Any files default max count | 20 | | Main image default size | 1920 x 1270 | | Main image default quality | 85 | | Thumbnail default size | 300 x 300 | | Thumbnail default quality | 80 |

Public API

The package exports:

export * from './uploader.module';
export * from './services/uploader.service';

export * from './decorators/dynamic-file.interceptor';
export * from './decorators/dynamic-files.interceptor';
export * from './decorators/dynamic-file-fields.interceptor';
export * from './decorators/dynamic-any-files.interceptor';

export * from './processors/sharp-image.processor';

export * from './interfaces/uploader-options.interface';
export * from './interfaces/uploaded-file-response.interface';
export * from './interfaces/file-post-processor.interface';
export * from './interfaces/file-validator.interface';

Important notes

  • This package currently uses local disk storage only.
  • Files are saved under process.cwd()/public/uploads.
  • The package generates public URLs, but your application must serve the upload directory.
  • sharp is a runtime dependency because SharpImageProcessor is imported directly by the service.
  • If you want sharp to become optional, the source code should be refactored to lazy-load the image processor only when image processing is enabled.

License

MIT