Receiving a File

FileInterceptor runs multer in the interceptor step, parses multipart/form-data, and hands your handler a file object.

SyntaxFileInterceptor(fieldName: string, localOptions?: MulterOptions): Type<NestInterceptor>

A JSON body cannot carry a file. A browser sends files as multipart/form-data: the body is cut into parts, one per form field, each with its own headers. Nest's JSON body parser ignores that format, so something else has to read the parts.

On the Express adapter that something is multer, wrapped by FileInterceptor() from @nestjs/platform-express. An interceptor is a class that wraps the call to your handler, able to run code before it and after it. FileInterceptor uses only the before half: it runs multer, waits until the upload has been fully read, then calls the handler.

Where it runs

Every request passes through middleware, then guards, then interceptors, then pipes, then the handler, then the interceptors again, then exception filters. The file is parsed in the interceptor step. A guard therefore runs before the file exists, and a pipe runs after the file is complete.

Where the bytes land

With neither dest nor storage set, multer holds the whole file in memory and sets file.buffer. With dest: './upload' it writes to disk under a random name and sets file.path, leaving buffer undefined. Nothing caps the size by default: multer's limits.fileSize is Infinity. When you set it and it is exceeded, NestJS 12 converts multer's LIMIT_FILE_SIZE into a PayloadTooLargeException (413).

Shorthand and longhand

@UploadedFile() reads file off the request object. These two handlers are the same thing:

upload(@UploadedFile() file: Express.Multer.File) {}
upload(@Req() req: Request) { const file = req.file; }

The Express.Multer.File type comes from @types/multer, a dev dependency you add yourself.

Example

Example · typescript
// src/uploads/uploads.controller.ts
import {
  Controller,
  Post,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('uploads')
export class UploadsController {
  @Post()
  // 'file' is the form field name the client used:
  //   form.append('file', blob, 'invoice.pdf')
  @UseInterceptors(
    FileInterceptor('file', {
      // No dest and no storage, so multer buffers the whole file in
      // memory and sets file.buffer. multer's own default for fileSize
      // is Infinity, so this line is the only thing stopping a 4 GB POST.
      limits: { fileSize: 5 * 1024 * 1024 }, // bytes, so 5 MiB
    }),
  )
  upload(@UploadedFile() file: Express.Multer.File) {
    // Exceeding limits.fileSize never reaches this method: multer errors
    // in the interceptor and Nest throws PayloadTooLargeException (413).

    // size and buffer are measured by the server. Everything else here
    // was typed by the client, originalname and mimetype included.
    return {
      originalName: file.originalname,
      declaredType: file.mimetype,
      bytes: file.size,
    };
  }
}

When to use it

  • Accept a profile picture from a browser form and return its stored size and type.
  • Take a CSV file plus an import mode field in the same POST.
  • Attach up to five receipts to an expense record in one request.
  • Cap a public upload route at 5 MiB without touching the other routes in the module.

More examples

Files and text fields in one form

multer puts the non-file parts of the same form into req.body, so @Body() keeps working next to @UploadedFiles(). The catch is that every multipart text field arrives as a string, so a DTO with a number or boolean needs class-transformer's @Type(() => Number) or a ValidationPipe configured with transform and implicit conversion. Reach for FilesInterceptor when one field carries several files, such as ticket attachments or gallery images; a sixth file fails the request with a 400.

Example · typescript
// src/tickets/tickets.controller.ts
import {
  Body,
  Controller,
  Post,
  UploadedFiles,
  UseInterceptors,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
import { CreateTicketDto } from './dto/create-ticket.dto';

@Controller('tickets')
export class TicketsController {
  @Post()
  // field name, max count, then the options object
  @UseInterceptors(
    FilesInterceptor('attachments', 5, {
      limits: { fileSize: 2 * 1024 * 1024 },
    }),
  )
  create(
    @Body() dto: CreateTicketDto,
    @UploadedFiles() files: Array<Express.Multer.File>,
  ) {
    return {
      subject: dto.subject,
      attachments: files.map((f) => f.originalname),
    };
  }
}

Module-wide defaults from configuration

register() takes a plain options object; registerAsync() takes a factory so the values can come from ConfigService. Either way it provides the defaults that every FileInterceptor in this module injects. Per-route options are merged over them, and in NestJS 12 limits is merged key by key, so a route that overrides limits.fileSize keeps the module's limits.files. Use this when the destination or the cap differs per environment, instead of hard-coding it at each route.

Example · typescript
// src/uploads/uploads.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MulterModule } from '@nestjs/platform-express';
import { UploadsController } from './uploads.controller';

@Module({
  imports: [
    MulterModule.registerAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        dest: config.getOrThrow<string>('UPLOAD_DIR'),
        limits: {
          fileSize: Number(config.getOrThrow<string>('UPLOAD_MAX_BYTES')),
          files: 10,
        },
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [UploadsController],
})
export class UploadsModule {}

The same route on the Fastify adapter

The @nestjs/platform-fastify/multipart subpath exists from NestJS 12.1 and needs the optional peer dependency @fastify/multipart (^10.1.0); the adapter registers the plugin itself when an upload interceptor is used. The decorators and the interceptor names are identical to the Express side, the types are renamed (MulterOptions becomes MultipartOptions, Express.Multer.File becomes UploadedMultipartFile), and the defaults differ. Read this one when you are porting upload routes between adapters.

Example · typescript
// src/uploads/uploads.controller.ts
import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import {
  FileInterceptor,
  UploadedMultipartFile,
} from '@nestjs/platform-fastify/multipart';

@Controller('uploads')
export class UploadsController {
  @Post()
  @UseInterceptors(
    // Fastify caps fileSize at the instance bodyLimit (1 MiB) rather than
    // leaving it unlimited, so raising it is as common as lowering it.
    FileInterceptor('file', { limits: { fileSize: 5 * 1024 * 1024 } }),
  )
  upload(@UploadedFile() file: UploadedMultipartFile) {
    return { bytes: file.size, type: file.mimetype };
  }
}

Discussion

  • Be the first to comment on this lesson.