NestJS: Type-safe File Uploads

Learn how to apply Swagger decorators for type-safe file upload endpoints.

Authors
Marc Stammerjohann Marc Stammerjohann
Published at

You will setup REST endpoints for uploading files, add Swagger decorators for type-safety and learn about Decorator composition to simplify Swagger decorators.

Before you start follow the setup for Swagger in your NestJS application.

The source code for this post is available in this repo on GitHub.

Get Started

Nest uses multer for handling file uploads using the multipart/form-data format.

Add the multer typings to improve type-safety.

npm i -D @types/multer

Upload File(s)

Start with uploading a single file. Add a new Post endpoint to your controller and add the FileInterceptor() to extract the file from the request. Gain access to the file payload via the @UploadedFile() decorator.

import {
  Controller,
  Post,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiTags } from '@nestjs/swagger';
import { FilesService } from './files.service';

@Controller('files')
@ApiTags('files')
export class FilesController {
  constructor(private readonly filesService: FilesService) {}

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

Start the Nest application npm run start:dev and checkout the new endpoint in your Swagger API localhost:3000/api.

Upload file without Swagger types

The endpoint is available but Swagger doesn't now anything about the file upload. Let's add the Swagger type definitions for uploading a file.

First, you add @ApiConsumes() to let Swagger now that this endpoint is consuming multipart/form-data. Now use @ApiBody() to enable file upload in the Swagger API.

import {
  Controller,
  Post,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { FilesService } from './files.service';

@Controller('files')
@ApiTags('files')
export class FilesController {
  constructor(private readonly filesService: FilesService) {}

  @Post('upload')
  @UseInterceptors(FileInterceptor('file')) // πŸ‘ˆ field name must match
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      type: 'object',
      properties: {
        file: { // πŸ‘ˆ this property
          type: 'string',
          format: 'binary',
        },
      },
    },
  })
  uploadFile(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
  }
}
The parameter for `@FileInterceptor()` must match the name of the properties field in the `@ApiBody()`. Otherwise Nest returns `400 Unexpected field`.

Swagger provides you now with a file selection πŸŽ‰.

Upload file without Swagger types

Now create endpoints for uploading array of files - @FilesInterceptor() and @UploadedFiles() - and multiple files - @FileFieldsInterceptor() and @UploadedFiles().

import {
  Controller,
  Post,
  UploadedFile,
  UploadedFiles,
  UseInterceptors,
} from '@nestjs/common';
import {
  FileFieldsInterceptor,
  FileInterceptor,
  FilesInterceptor,
} from '@nestjs/platform-express';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { FilesService } from './files.service';

@Controller('files')
@ApiTags('files')
export class FilesController {
  constructor(private readonly filesService: FilesService) {}

  @Post('upload')
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      type: 'object',
      properties: {
        file: {
          type: 'string',
          format: 'binary',
        },
      },
    },
  })
  uploadFile(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
  }

  @Post('uploads')
  @UseInterceptors(FilesInterceptor('files')) // πŸ‘ˆ  using FilesInterceptor here
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      type: 'object',
      properties: {
        files: {
          type: 'array', // πŸ‘ˆ  array of files
          items: {
            type: 'string',
            format: 'binary',
          },
        },
      },
    },
  })
  uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>) {
    console.log(files);
  }

  @Post('uploadFields')
  @UseInterceptors(
    FileFieldsInterceptor([ // πŸ‘ˆ  multiple files with different field names 
      { name: 'avatar', maxCount: 1 },
      { name: 'background', maxCount: 1 },
    ]),
  )
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      type: 'object',
      properties: { 
        // πŸ‘ˆ  field names need to be repeated for swagger
        avatar: {
          type: 'string',
          format: 'binary',
        },
        background: {
          type: 'string',
          format: 'binary',
        },
      },
    },
  })
  uploadMultipleFiles(@UploadedFiles() files: Express.Multer.File[]) {
    console.log(files);
  }
}

Checkout the new endpoints in your Swagger API.

Upload array of files with Swagger types

Upload array of files with Swagger types

As you noticed you need to add a few decorators to your endpoints and repeat the definition again for Swagger to pick up the correct file types. This is quite error prone as you might forget to add decorator or use the wrong file name property.

Let's improve it by creating custom decorators for file uploads and combining all required decorators together.

File upload decorators

Create a new file called api-file.decorator.ts and export a function called ApiFile which returns applyDecorators() provided by Nest. Copy all decorators required for handling file upload FileInterceptor(), @ApiConsumes and ApiBody into applyDecorators().

import { applyDecorators, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiBody, ApiConsumes } from '@nestjs/swagger';

export function ApiFile() {
  return applyDecorators(
    UseInterceptors(FileInterceptor('file')),
    ApiConsumes('multipart/form-data'),
    ApiBody({
      schema: {
        type: 'object',
        properties: {
          file: {
            type: 'string',
            format: 'binary',
          },
        },
      },
    }),
  );
}

Now you can replace those decorators and use @ApiFile() instead.

import {
  Controller,
  Post,
  UploadedFile,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ApiFile } from './api-file.decorator';
import { FilesService } from './files.service';

@Controller('files')
@ApiTags('files')
export class FilesController {
  constructor(private readonly filesService: FilesService) {}

  @Post('upload')
  @ApiFile() // πŸ€™ cleaned up decorators
  uploadFile(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
  }

What if you want to upload a file with a different field name than file? Add a fieldName parameter to ApiFile and set the default to file. Replace file with the new fieldName property. You can even go a step further and add required and MulterOptions as optional parameters.

import { applyDecorators, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
import { ApiBody, ApiConsumes } from '@nestjs/swagger';

export function ApiFile(
  fieldName: string = 'file',
  required: boolean = false,
  localOptions?: MulterOptions,
) {
  return applyDecorators(
    UseInterceptors(FileInterceptor(fieldName, localOptions)),
    ApiConsumes('multipart/form-data'),
    ApiBody({
      schema: {
        type: 'object',
        required: required ? [fieldName] : [],
        properties: {
          [fieldName]: {
            type: 'string',
            format: 'binary',
          },
        },
      },
    }),
  );
}

This solves the problem that the fieldName for the FileInterceptor and the ApiBody are always the same and is convenient and easy to reuse.

import {
  Controller,
  Post,
  UploadedFile,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ApiFile } from './api-file.decorator';
import { FilesService } from './files.service';

@Controller('files')
@ApiTags('files')
export class FilesController {
  constructor(private readonly filesService: FilesService) {}

  @Post('upload')
  @ApiFile('avatar', true) // 🀩 changing field name and set file required
  uploadFile(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
  }
}

Now create decorators for @ApiFiles() and @ApiFileFields().

import { applyDecorators, UseInterceptors } from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
import { ApiBody, ApiConsumes } from '@nestjs/swagger';

export function ApiFiles(
  fieldName: string = 'files',
  required: boolean = false,
  maxCount: number = 10,
  localOptions?: MulterOptions,
) {
  return applyDecorators(
    UseInterceptors(FilesInterceptor(fieldName, maxCount, localOptions)),
    ApiConsumes('multipart/form-data'),
    ApiBody({
      schema: {
        type: 'object',
        required: required ? [fieldName] : [],
        properties: {
          [fieldName]: {
            type: 'array',
            items: {
              type: 'string',
              format: 'binary',
            },
          },
        },
      },
    }),
  );
}
import { applyDecorators, UseInterceptors } from '@nestjs/common';
import { FileFieldsInterceptor } from '@nestjs/platform-express';
import {
  MulterField,
  MulterOptions,
} from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
import { ApiBody, ApiConsumes } from '@nestjs/swagger';
import {
  ReferenceObject,
  SchemaObject,
} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';

export type UploadFields = MulterField & { required?: boolean };

export function ApiFileFields(
  uploadFields: UploadFields[],
  localOptions?: MulterOptions,
) {
  const bodyProperties: Record<string, SchemaObject | ReferenceObject> =
    Object.assign(
      {},
      ...uploadFields.map((field) => {
        return { [field.name]: { type: 'string', format: 'binary' } };
      }),
    );
  const apiBody = ApiBody({
    schema: {
      type: 'object',
      properties: bodyProperties,
      required: uploadFields.filter((f) => f.required).map((f) => f.name),
    },
  });

  return applyDecorators(
    UseInterceptors(FileFieldsInterceptor(uploadFields, localOptions)),
    ApiConsumes('multipart/form-data'),
    apiBody,
  );
}

Now compare the endpoints without and with custom file upload decorators

import {
  Controller,
  Post,
  UploadedFile,
  UploadedFiles,
  UseInterceptors,
} from '@nestjs/common';
import {
  FileFieldsInterceptor,
  FileInterceptor,
  FilesInterceptor,
} from '@nestjs/platform-express';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { FilesService } from './files.service';

@Controller('files')
@ApiTags('files')
export class FilesController {
  constructor(private readonly filesService: FilesService) {}

  @Post('upload')
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      type: 'object',
      properties: {
        file: {
          type: 'string',
          format: 'binary',
        },
      },
    },
  })
  uploadFile(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
  }

  @Post('uploads')
  @UseInterceptors(FilesInterceptor('files')) // πŸ‘ˆ  using FilesInterceptor here
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      type: 'object',
      properties: {
        files: {
          type: 'array', // πŸ‘ˆ  array of files
          items: {
            type: 'string',
            format: 'binary',
          },
        },
      },
    },
  })
  uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>) {
    console.log(files);
  }

  @Post('uploadFields')
  @UseInterceptors(
    FileFieldsInterceptor([ // πŸ‘ˆ  multiple files with different field names 
      { name: 'avatar', maxCount: 1 },
      { name: 'background', maxCount: 1 },
    ]),
  )
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      type: 'object',
      properties: { 
        // πŸ‘ˆ  field names need to be repeated for swagger
        avatar: {
          type: 'string',
          format: 'binary',
        },
        background: {
          type: 'string',
          format: 'binary',
        },
      },
    },
  })
  uploadMultipleFiles(@UploadedFiles() files: Express.Multer.File[]) {
    console.log(files);
  }
}
import { Controller, Post, UploadedFile, UploadedFiles } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ApiFileFields } from './api-file-fields.decorator';
import { ApiFile } from './api-file.decorator';
import { ApiFiles } from './api-files.decorator';
import { FilesService } from './files.service';

@Controller('files')
@ApiTags('files')
export class FilesController {
  constructor(private readonly filesService: FilesService) {}

  @Post('upload')
  @ApiFile('avatar', false)
  uploadFile(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
  }

  @Post('uploads')
  @ApiFiles('files', true)
  uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>) {
    console.log(files);
  }

  @Post('uploadFields')
  @ApiFileFields([
    { name: 'avatar', maxCount: 1, required: true },
    { name: 'background', maxCount: 1 },
  ])
  uploadMultipleFiles(@UploadedFiles() files: Express.Multer.File[]) {
    console.log(files);
  }
}

Custom file filter

What if you like to allow only images or PDF's to upload? Thats where the MulterOptions.fileFilter come into action. You can filter based on the file properties such as originalname, mimetype, size and more.

Let's create a filter for mimetypes call the function fileMimetypeFilter which receives one or more mimetypes to match, use the spread operator for the parameter. The fileMimetypeFilter return and implements the multer filter signature.

import { UnsupportedMediaTypeException } from '@nestjs/common';

export function fileMimetypeFilter(...mimetypes: string[]) {
  return (
    req,
    file: Express.Multer.File,
    callback: (error: Error | null, acceptFile: boolean) => void,
  ) => {
    if (mimetypes.some((m) => file.mimetype.includes(m))) {
      callback(null, true);
    } else {
      callback(
        new UnsupportedMediaTypeException(
          `File type is not matching: ${mimetypes.join(', ')}`,
        ),
        false,
      );
    }
  };
}

Add the filter to the @ApiFile(), @ApiFiles() or @ApiFileFields() decorators localOptions object.

import {
  Controller,
  Post,
  UploadedFile,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ApiFile } from './api-file.decorator';
import { FilesService } from './files.service';
import { fileMimetypeFilter } from './file-mimetype-filter';

@Controller('files')
@ApiTags('files')
export class FilesController {
  constructor(private readonly filesService: FilesService) {}

  @Post('upload')
  @ApiFile('avatar', true, { fileFilter: fileMimetypeFilter('image') }) 
  uploadFile(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
  }
}

This endpoint only accepts files which include the mimetype image such as image/jpeg or image/png. This is already very quick and the fileMimetypeFilter can be reused.

But heck why not create custom decorators based on the supported mimetype. Let's create two example decorators: ApiImageFile and ApiPdfFile. They are simple functions returning the previous created ApiFile (or ApiFiles) decorator and specifying the fileMimetypeFilter().

import { fileMimetypeFilter } from './file-mimetype-filter';

export function ApiImageFile(
  fileName: string = 'image',
  required: boolean = false,
) {
  return ApiFile(fileName, required, {
    fileFilter: fileMimetypeFilter('image'),
  });
}

export function ApiPdfFile(
  fileName: string = 'document',
  required: boolean = false,
) {
  return ApiFile(fileName, required, {
    fileFilter: fileMimetypeFilter('pdf'),
  });
}

Now simply use @ApiImageFile or @ApiPdfFile to handle file uploads.

import {
  Controller,
  Post,
  UploadedFile,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ApiImageFile, ApiPdfFile  } from './api-file.decorator';
import { FilesService } from './files.service';

@Controller('files')
@ApiTags('files')
export class FilesController {
  constructor(private readonly filesService: FilesService) {}

  @Post('avatar')
  @ApiImageFile('avatar', true)
  uploadAvatar(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
  }
  
  @Post('document')
  @ApiPdfFile('document', true)
  uploadDocument(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
  }
}

File validation

Until now, files are only set to required for the Swagger API. If you call the endpoint from a web framework or REST client like Insomnia you are receiving a 201 status and the received file is undefined.

To validate, a file is not undefined create a custom pipe, let's called it ParseFile. The pipe will check if the file is provided or not and throw a 400 Bad Request exception.

import {
  ArgumentMetadata,
  Injectable,
  PipeTransform,
  BadRequestException,
} from '@nestjs/common';

@Injectable()
export class ParseFile implements PipeTransform {
  transform(
    files: Express.Multer.File | Express.Multer.File[],
    metadata: ArgumentMetadata,
  ): Express.Multer.File | Express.Multer.File[] {
    if (files === undefined || files === null) {
      throw new BadRequestException('Validation failed (file expected)');
    }

    if (Array.isArray(files) && files.length === 0) {
      throw new BadRequestException('Validation failed (files expected)');
    }

    return files;
  }
}

Pass the ParseFile to the @UploadFile() or @UploadFiles() decorator and you now receive a 400 Bad Request if the file is not provided.

import {
  Controller,
  Post,
  UploadedFile,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ApiFile } from './api-file.decorator';
import { FilesService } from './files.service';
import { ParseFile } from './parse-file.pipe';

@Controller('files')
@ApiTags('files')
export class FilesController {
  constructor(private readonly filesService: FilesService) {}

  @Post('upload')
  @ApiFile() 
  // πŸ”Ž ParseFile and throw 400 if file not provided
  uploadFile(@UploadedFile(ParseFile) file: Express.Multer.File) {
    console.log(file);
  }
}

That's it for this post. Enjoy uploading your files to Nest! Where are you storing your uploaded files? Drop a comment below if you like.

Table of Contents

Top of Page Sorry, could not load static page content Comments Related Articles

Related Posts

Find more posts like this one.

Authors
Marc Stammerjohann
August 26, 2021

OpenApi for your REST APIs in NestJS

Setup Swagger to generate an OpenApi documentation for your REST endpoints.
NestJS Read More
Authors
Marc Stammerjohann
August 16, 2021

Send Emails with NestJS

Create Email Templates and send them with nodemailer from your Nest application
NestJS Read More
Authors
Marc Stammerjohann
August 17, 2021

Introducing NestJS Prisma Library and Schematics

Library and schematics to add Prisma integration to a NestJS application
NestJS Prisma Read More
Authors
Marc Stammerjohann
June 03, 2021

Dockerizing a NestJS app with Prisma and PostgreSQL

How to dockerize a NestJS application with Prisma and PostgreSQL.
NestJS Prisma Docker Read More
Authors
Marc Stammerjohann
April 07, 2020

GraphQL Code-First Approach with NestJS 7

Create a GraphQL API using Code-First Approach with NestJS 7.
NestJS GraphQL Prisma Read More
Authors
Marc Stammerjohann
April 07, 2020

Deploy NestJS with Prisma to Heroku

Deploy a NestJS application with Prisma 2.0 to Heroku and connect to a PostgreSQL database.
NestJS Prisma Heroku Read More
Authors
Marc Stammerjohann
June 17, 2021

How to query your database using Prisma with NestJS

Learn how to setup a database with Prisma 2.0 and query data using NestJS.
NestJS Prisma Read More

Sign up for our newsletter

Sign up for our newsletter to stay up to date. Sent every other week.

We care about the protection of your data. Read our Privacy Policy.