Skip to content

@vup/nest-upload Upload Module

A reusable NestJS upload module for monorepo, defaulting to local storage, providing minimal viable encapsulation of Service + Controller. Supports scene-based configuration, file type restrictions, size limits, and more.

Technical Stack

  • NestJS - Node.js enterprise framework
  • TypeORM - TypeScript ORM
  • Multer - File upload middleware

Quick Start

Installation

In NestJS projects, this package is usually available through monorepo workspace and doesn't need separate installation.

Basic Usage

Import and configure in business module:

ts
import { Module } from '@nestjs/common';
import { UploadModule } from '@vup/nest-upload';

@Module({
  imports: [
    UploadModule.forRoot({
      uploadDir: 'uploads',
      baseUrl: 'http://localhost:3000/uploads',
    }),
  ],
})
export class AppModule {}

Upload File

After configuration, you can use the upload interface:

bash
# Upload file
POST /upload
Content-Type: multipart/form-data

file: [file]

Configuration Options

UploadModuleOptions

ParameterDescriptionTypeDefault
uploadDirUpload directory (relative to project root)string'uploads'
baseUrlAccess prefix (for concatenating file URL)string-
maxSizeMaximum file size (bytes)number10 * 1024 * 1024 (10MB)
allowedTypesAllowed MIME typesstring[]-
allowedSuffixesAllowed file suffixes (without dot)string[]-
scenesScene-level configuration overrideRecord<string, SceneConfig>-

Scene Configuration (SceneConfig)

Each scene can override global configuration:

ts
scenes: {
  avatar: {
    maxSize: 2 * 1024 * 1024,        // 2MB
    allowedTypes: ['image/png', 'image/jpeg'],
    allowedSuffixes: ['png', 'jpg', 'jpeg'],
  },
  video: {
    maxSize: 500 * 1024 * 1024,       // 500MB
    allowedTypes: ['video/mp4'],
    allowedSuffixes: ['mp4'],
  },
}

Complete Configuration Example

ts
import { Module } from '@nestjs/common';
import { UploadModule } from '@vup/nest-upload';

@Module({
  imports: [
    UploadModule.forRoot({
      uploadDir: 'uploads',
      baseUrl: 'https://your-domain.com/uploads',
      maxSize: 10 * 1024 * 1024, // 10MB
      allowedSuffixes: ['png', 'jpg', 'jpeg', 'pdf'],
      scenes: {
        avatar: {
          maxSize: 2 * 1024 * 1024,
          allowedTypes: ['image/png', 'image/jpeg'],
        },
        document: {
          maxSize: 5 * 1024 * 1024,
          allowedSuffixes: ['pdf', 'doc', 'docx'],
        },
        video: {
          maxSize: 500 * 1024 * 1024,
          allowedTypes: ['video/mp4'],
        },
      },
    }),
  ],
})
export class AppModule {}

HTTP Interfaces

Upload File

http
POST /upload?scene=avatar
Content-Type: multipart/form-data

file: [file]

Response Example:

json
{
  "id": "123456",
  "filename": "avatar.jpg",
  "originalName": "my-avatar.jpg",
  "mimeType": "image/jpeg",
  "size": 102400,
  "url": "https://your-domain.com/uploads/avatar/2024-01-19/avatar.jpg",
  "scene": "avatar",
  "createdAt": "2024-01-19T10:00:00.000Z"
}

Get File List

http
GET /upload/list?scene=avatar&page=1&limit=10

Response Example:

json
{
  "data": [
    {
      "id": "123456",
      "filename": "avatar.jpg",
      "url": "https://your-domain.com/uploads/avatar/2024-01-19/avatar.jpg",
      "size": 102400,
      "createdAt": "2024-01-19T10:00:00.000Z"
    }
  ],
  "total": 100,
  "page": 1,
  "limit": 10
}

Get File Details

http
GET /upload/:id

Delete File

http
DELETE /upload/:id

File Storage Structure

Files are stored in the following structure:

{uploadDir}/{scene}/{YYYY-MM-DD}/{filename}

For example:

uploads/
  ├─ avatar/
  │  ├─ 2024-01-19/
  │  │  ├─ avatar-001.jpg
  │  │  └─ avatar-002.jpg
  │  └─ 2024-01-20/
  │     └─ avatar-003.jpg
  ├─ document/
  │  └─ 2024-01-19/
  │     └─ doc-001.pdf
  └─ default/
     └─ 2024-01-19/
        └─ file-001.png

When scene is not provided, the default scene default is used.

Custom Entity

If you need to extend the table structure, you can extend BaseUploadEntity:

ts
import { BaseUploadEntity } from '@vup/nest-upload';
import { Column, Entity } from 'typeorm';

@Entity('custom_upload_files')
export class CustomUploadEntity extends BaseUploadEntity {
  @Column({ name: 'biz_type', length: 64 })
  bizType!: string;

  @Column({ name: 'biz_id', length: 64 })
  bizId!: string;

  @Column({ name: 'user_id', length: 64, nullable: true })
  userId?: string;
}

Then specify in UploadModule configuration:

ts
UploadModule.forRoot({
  entity: CustomUploadEntity,
  uploadDir: 'uploads',
  baseUrl: 'http://localhost:3000/uploads',
});

Note: TypeORM needs to register this entity in the business project's TypeOrmModule.forRoot.

Using Service

In addition to HTTP interfaces, you can also directly use UploadService:

ts
import { Injectable } from '@nestjs/common';
import { UploadService } from '@vup/nest-upload';

@Injectable()
export class MyService {
  constructor(private readonly uploadService: UploadService) {}

  async uploadFile(file: Express.Multer.File, scene?: string) {
    return await this.uploadService.upload(file, scene);
  }

  async getFileList(scene?: string, page = 1, limit = 10) {
    return await this.uploadService.getList(scene, page, limit);
  }

  async deleteFile(id: string) {
    return await this.uploadService.delete(id);
  }
}

Frontend Integration Examples

Upload Using FormData

typescript
async function uploadFile(file: File, scene?: string) {
  const formData = new FormData();
  formData.append('file', file);

  const url = scene ? `/upload?scene=${scene}` : '/upload';

  const response = await fetch(url, {
    method: 'POST',
    body: formData,
  });

  return await response.json();
}

// Usage example
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
  const file = (e.target as HTMLInputElement).files?.[0];
  if (file) {
    const result = await uploadFile(file, 'avatar');
    console.log('Upload successful:', result);
  }
});

Upload Using axios

typescript
import axios from 'axios';

async function uploadFile(file: File, scene?: string) {
  const formData = new FormData();
  formData.append('file', file);

  const { data } = await axios.post(
    scene ? `/upload?scene=${scene}` : '/upload',
    formData,
    {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    }
  );

  return data;
}

Notes

  • Default uses LocalStorageAdapter, files stored in local file system
  • TypeORM needs to be configured to use database functionality
  • File size and type restrictions are set during configuration and automatically validated during upload
  • It's recommended to configure baseUrl as the actual domain in production environment
  • Date format in file paths is YYYY-MM-DD for easy date-based file management
  • If you need to use cloud storage (OSS, S3, etc.), you can implement a custom StorageAdapter