Files
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.
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'.
Press Run tests to start the app. Its log appears here.Graded endpoints
A cat without a photo
FileInterceptor parsed the multipart body: the file reached the handler through the validators, and the caption field arrived in the body
The client said image/png; the bytes have no PNG signature, and FileTypeValidator believes the bytes. The message repeats the type the client claimed
MaxFileSizeValidator runs first and names both sizes
The form carried a caption and no photo; ParseFilePipe requires one
A file in a field the interceptor did not ask for is multer's Unexpected field
The row remembers the file's name, size and type
FilesInterceptor collected both files from the same field
maxCount is two; the third is an Unexpected field