File UploadTechniques · NestJS

Accept a photo for a cat: parse multipart with FileInterceptor, validate the file's size and real type with ParseFilePipe, read the fields beside it, and take several documents at once with FilesInterceptor.

What you will learn

Read the theory for File Upload

All Techniques lessons

All NestJS courses

loading types…

What you'll learn

  • Receive one uploaded file with FileInterceptor and @UploadedFile(), and the form's fields with @Body()
  • Validate an upload's size and real type with ParseFilePipe, MaxFileSizeValidator and FileTypeValidator
  • Receive several files with FilesInterceptor and a maximum count

File Upload

A cat deserves a photo, and a photo is not JSON. Browsers send files as multipart/form-data, a body made of parts, each with its own headers, where a part may be a plain field or a file with a name and a content type. Express handles that format with the multer middleware, and Nest wraps multer in interceptors so that a handler receives the file as a parameter, the way it receives a body. The rest of the lesson is what to do with an upload you do not trust yet, which is every upload.

One file

FileInterceptor(fieldName) from @nestjs/platform-express parses the request and puts the file from that field on the request; @UploadedFile() hands it to the handler:

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
  console.log(file);
}

Express.Multer.File is what multer builds: fieldname, originalname (the client's file name), mimetype (the client's declared type), size, and buffer, the bytes, because Nest's default is multer's memory storage. The other fields of the form arrive as usual in @Body(), strings all, since multipart has no types. A file in a field the interceptor did not ask for is a 400 Unexpected field; a request that is not multipart at all is passed through with no file.

The second argument takes multer's options: limits: { fileSize } rejects a large file with 413 File too large before it is fully read, fileFilter accepts or drops a file by its metadata. MulterModule.register({ ... }) sets options for every interceptor in a module.

Validating the file

A file that arrived is not a file you want. ParseFilePipe runs validators on the uploaded file the way ValidationPipe runs decorators on a body:

@UploadedFile(
  new ParseFilePipe({
    validators: [
      new MaxFileSizeValidator({ maxSize: 1000 }),
      new FileTypeValidator({ fileType: 'image/jpeg' }),
    ],
  }),
)
file: Express.Multer.File,

MaxFileSizeValidator checks size. FileTypeValidator does more than compare mimetype, which the client chose: it reads the file's first bytes, the magic numbers every real format starts with, and checks that they say what the type claims. A text file renamed photo.png and sent as image/png is refused, and the message is worth reading twice: Validation failed (current file type is image/png, expected type is image/png). It repeats the type the client claimed, because that is all the validator can name; the refusal came from the bytes, which carry no PNG signature. A missing file fails with File is required, unless fileIsRequired: false. ParseFilePipeBuilder builds the same pipe fluently, with errorHttpStatusCode to answer 422 instead of 400.

Several files

FilesInterceptor(fieldName, maxCount) collects an array from one field, on @UploadedFiles(). FileFieldsInterceptor([{ name: 'avatar', maxCount: 1 }, { name: 'background', maxCount: 1 }]) collects from several named fields into an object keyed by field. AnyFilesInterceptor() takes every file whatever its field. NoFilesInterceptor() parses the fields of a multipart form and refuses any file with 400 Unexpected field.

Where it sits

An interceptor runs before pipes: middleware → guards → interceptors → pipes → handler. That order is what makes this work: the interceptor parses the multipart body and puts the file on the request, then ParseFilePipe, a pipe, validates what @UploadedFile() picked up. Put the validator in the parameter, never in a guard, or it runs before there is anything to validate.

Your task

Cats get a photo and their paperwork.

  1. POST /cats/:id/photo takes one file from the field photo, a PNG of at most 100 bytes, required, validated with ParseFilePipe; the caption field travels beside it.
  2. POST /cats/:id/documents takes up to two files from the field documents.

The request panel cannot attach files, so the graded requests carry them: a 70-byte PNG, a text file in PNG's clothing, and one document too many. Run and read each answer.

When it fails

  • TypeError: Cannot read properties of undefined (reading 'originalname'): no interceptor parsed the request, so @UploadedFile() was undefined. Add @UseInterceptors(FileInterceptor('photo')).
  • 400 Unexpected field: the file was sent in a field the interceptor did not name, or one file too many for maxCount.
  • A text file is accepted as a photo: no validator, or FileTypeValidator compares the client's mimetype only because skipMagicNumbersValidation is set.
  • current file type is image/png, expected type is image/png: not a contradiction. The client claimed PNG and the validator names that claim; the bytes were not a PNG.
  • 400 File is required on a request that did send one: the field name in the request and in the interceptor differ.

Remember

  • @UseInterceptors(FileInterceptor('field')) plus @UploadedFile() gives a handler one Express.Multer.File, bytes in buffer.
  • ParseFilePipe with MaxFileSizeValidator and FileTypeValidator validates the file itself; the type check reads magic numbers.
  • FilesInterceptor, FileFieldsInterceptor, AnyFilesInterceptor and NoFilesInterceptor cover the other shapes.
  • Interceptors run before pipes, so the file exists by the time the validator looks at it.
Stuck? Show a hint

@UseInterceptors(FileInterceptor('photo')) on the route; @UploadedFile(new ParseFilePipe({ validators: [new MaxFileSizeValidator({ maxSize: 100 }), new FileTypeValidator({ fileType: 'image/png' })] })) on the parameter. Documents: @UseInterceptors(FilesInterceptor('documents', 2)) with @UploadedFiles(). Both interceptors come from '@nestjs/platform-express'.