File Upload
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'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
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.
POST /cats/:id/phototakes one file from the fieldphoto, a PNG of at most 100 bytes, required, validated withParseFilePipe; thecaptionfield travels beside it.POST /cats/:id/documentstakes up to two files from the fielddocuments.
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()wasundefined. Add@UseInterceptors(FileInterceptor('photo')).400 Unexpected field: the file was sent in a field the interceptor did not name, or one file too many formaxCount.- A text file is accepted as a photo: no validator, or
FileTypeValidatorcompares the client'smimetypeonly becauseskipMagicNumbersValidationis 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 requiredon 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 oneExpress.Multer.File, bytes inbuffer.ParseFilePipewithMaxFileSizeValidatorandFileTypeValidatorvalidates the file itself; the type check reads magic numbers.FilesInterceptor,FileFieldsInterceptor,AnyFilesInterceptorandNoFilesInterceptorcover 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'.