Skip to main content

Uploading in controllers

FileStorageInterceptor(field, options?) accepts multipart/form-data, stores the file(s), and writes the result into request.body.

Upload modes

// Single file (field name 'file')
@UseInterceptors(FileStorageInterceptor('file'))
upload(@Body() body: { file: string }) {}

// Multiple files in one field
@UseInterceptors(FileStorageInterceptor({ type: 'array', fieldName: 'photos', maxCount: 5 }))
uploadMany(@Body() body: { photos: string[] }) {}

// Multiple named fields
@UseInterceptors(FileStorageInterceptor({
type: 'fields',
fields: [{ name: 'avatar', maxCount: 1 }, { name: 'docs', maxCount: 10 }],
}))
uploadFields(@Body() body: { avatar: string[]; docs: string[] }) {}
ModeAccepted field(s)Default body value
'file' (string)filebody.file: string
{ type: 'array' }repeated photos or photos[0], photos[1], …body.photos: string[]
{ type: 'fields' }each named fieldbody[field]: string[]
note

In fields mode every field is mapped to an array, even with maxCount: 1. Your DTO should match — string for single, string[] for array/fields.

Control the stored key

import { extname } from 'path';

FileStorageInterceptor('avatar', {
fileDist: (_file, req) => `users/${req.user.id}/avatars`, // folder (relative)
fileName: (file) => `${Date.now()}${extname(file.originalname)}`, // last segment
prefix: 'public', // optional static prefix
});
// -> public/users/42/avatars/1713876155123.png

The final key is joinKey(prefix, fileDist, fileName). Defaults: fileDist = YYYY/MM/DD, fileName = uuid-originalname. Both callbacks can be async.

Choose the driver per route

FileStorageInterceptor('file', { driver: 's3' }); // by name
FileStorageInterceptor('file', { driver: (req) => req.user.plan }); // dynamically
FileStorageInterceptor('file', { tenant: false }); // opt out of tenant routing

Precedence: an explicit driver wins, then tenant resolution, then the module default.

Map the result into the body

By default the interceptor writes the storage key. Customize with mapToRequestBody:

FileStorageInterceptor('document', {
mapToRequestBody: (file) => ({ key: file.key, url: file.url, size: file.size }),
});
// body.document === { key, url, size }

Set overwriteBodyField: false to keep an existing body value (e.g. a JSON field with the same name on a PATCH).

Post-upload hook

Run cross-field checks or side effects after files are stored and before your handler:

FileStorageInterceptor({ type: 'fields', fields: [{ name: 'images', maxCount: 10 }] }, {
afterUpload: (req) => {
const files = req.files as Record<string, Express.Multer.File[]>;
if ((files?.images?.length ?? 0) < 2) {
throw new BadRequestException('At least 2 images are required.');
}
},
});