Uploading files in NestJS

Oyugo Obonyo
6 min readJul 3, 2023

--

Be it uploading profile pictures, reports or even video reels, you can only avoid a media upload-and-download module in your application for so long. In this post, we’ll explore how to do file uploads in NestJS.

The code referenced in this post can be found within this GitHub repository.

What is NestJS?

NestJS is humankind’s greatest discovery after fire. It is a NodeJs framework that allows users to build server-side applications in the fanciest, neatest and cleanest of ways.

This blog post assumes that you already have NestJS installed in your computer and have operational knowledge of the framework.

Setting up our application

First, we’ll start by creating a new NestJS project by running the following command on your terminal (Replace your-project-name with whichever name for your new project then run the command below):

$ nest new your-project-name

The command takes a while to run but once it's done, we get a whole lot of generated files and folders common to every NestJs project. The application’s structure should resemble this:

Initial application structure

Creating our upload endpoint

Now that the application is set up, we can set up our upload file controller. Usually, such a controller, along with service and repository classes related to it, would be in a separate standalone module. However, since we’ll only have one endpoint, we can have the upload user controller in our app.controller.ts file. NestJS automatically generated an app.service.ts because it expects us to adopt the controller—service pattern that works in the following way:
1. The controller layer: -Presents our data to the users. This is just a presentation layer and it typically doesn't have business logic or data source access logic.
2. The service layer:- Contains the business logic of the application such as condition checks or input processing. As things currently stand, this layer is also expected to contain the access logic to the data layer.
In our case, we won’t need a service layer so we can go ahead and delete the app.service.ts file.

Accepting files on our app controller

NestJS’ amazing documentation gives explicit instructions on how you can prepare your application to handle file uploading. To accept file uploads, we can alter our AppController in src/app.controller.ts:

import {
Controller,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { AppService } from './app.service';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
public async uploadFile(@UploadedFile() file) {
return file;
}
}

At the moment, the upload method intercepts an uploaded file and returns the file details to the user. The field name of the uploaded file has to be the value passed as an argument to the FileInterceptor() class, which in our case is file. We can now make our first upload attempt by running our server with the following command:

$ yarn run start:dev

then upload the file via Postman:

postman request to upload file via our ‘upload’ endpoint

File validation

Currently, our upload endpoint accepts all file uploads. Imagine a very odd scenario where a consumer of this endpoint would choose to send a 15 MB pdf document as a profile picture upload. Validators ensure that only files which pass all the validation requirements are successfully uploaded. Luckily, NestJS already defines commonly used validators i.e. file size and file type validators that we can use:

import {
Controller,
Post,
UploadedFile,
UseInterceptors,
ParseFilePipeBuilder,
HttpStatus,
} from '@nestjs/common';
import { AppService } from './app.service';
import { FileInterceptor } from '@nestjs/platform-express';

const MAX_PROFILE_PICTURE_SIZE_IN_BYTES = 2 * 1024 * 1024;

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
public async uploadFile(
@UploadedFile(
new ParseFilePipeBuilder()
.addFileTypeValidator({ fileType: 'image/jpeg' })
.addMaxSizeValidator({ maxSize: MAX_PROFILE_PICTURE_SIZE_IN_BYTES })
.build({ errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY }),
)
file,
) {
return file;
}
}

The set validator ensures that only files with a maximum size of 2MB and have their mimetype property as image/jpeg will be accepted as valid uploads. Our upload will fail should we try to upload a file that doesn’t meet all the set requirements:

Upload fails in case file is bigger than the set maximum size or is of any other type but image/jpeg

Faulty file type validator

addFileTypeValidator() seems to be working so far but there is a major fault. The method determines a file’s type not by checking its content, but by deriving it from the file’s extension in the user’s device. This means that users can arbitrarily assign extensions to invalid types therefore bypassing the set validation rules. You can test this out by renaming a file with an invalid type i.e a pdf file to a file with a valid extension. For example, our previous upload would be successful were I to rename the file to Bruno_Obonyo_SWE_Resume_.jpg.

Safer file type validator

As things stand, the current file type validator implementation poses a potential security risk as the set rules could be easily bypassed by users. To correct this, we must abandon NestJs’ validator and create a custom file type validator. In a new file, src/app.validators.ts, add the following block:

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

export interface CustomUploadTypeValidatorOptions {
fileType: string[];
}

export class CustomUploadFileTypeValidator extends FileValidator {
private _allowedMimeTypes: string[];

constructor(protected readonly validationOptions: CustomUploadTypeValidatorOptions) {
super(validationOptions);
this._allowedMimeTypes = this.validationOptions.fileType;
}

public isValid(file?: any): boolean {}

public buildErrorMessage(): string {
return `Upload not allowed. Upload only files of type: ${this._allowedMimeTypes.join(
', ',
)}`;
}
}

Our custom file type validator must extend NestJs’ FileValidator so that it can appropriately conform to type constraints where necessary. As FileValidator is an abstract class, any class that implements is must implement the following abstract methods:
1. isValid:- A function that defines the validation mechanism of our custom validator and return a boolean depending on the validity of our file — True in case of a valid file and false in case the file is invalid.

2. buildErrorMessage:- A function that return a custom error message in case our validation fails.

Rather than naively determining a file’s type by checking its extension as is the case with the current validator, our custom validator should determine a file’s type by checking its magic number. A file’s magic number is a unique sequence of bytes at the beginning of a file that helps in the determination of the file’s format. Luckily, a couple of libraries that can help us parse file content and check their magic numbers already exist. For our case, we will use the file-type-mime library that is installed as follows:

$ yarn add file-type-mime

After its successful installation, we can now use it and implement our isValid() method:

...
import { Express } from 'express'
import * as fileType from 'file-type-mime';

...

public isValid(file?: Express.Multer.File): boolean {
// If the next uncommented line doesn't work due to package updates on file-type-mime,
// try this line instead:
// const response = fileType.parse(file.buffer);
// <--Correction courtesy of Sven Stadhouders from the comments-->
const response = fileType.default(file.buffer);
return this._allowedMimeTypes.includes(response.mime);
}

...

Apart from adding the library implementation, we’ve also changed the argument's type of the isValid method from any to Express.Multer.File for better type safety. For this type annotation to correctly work, the Multer typing package has to be first installed:

$ yarn add @types/multer -D

Adding custom validator to upload files endpoint

With our custom file type validator done, it now time to add it to our upload files endpoint in the src/app.controller.ts file:

import {
Controller,
Post,
UploadedFile,
UseInterceptors,
ParseFilePipeBuilder,
HttpStatus,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { CustomUploadFileTypeValidator } from './app.validators';

const MAX_PROFILE_PICTURE_SIZE_IN_BYTES = 2 * 1024 * 1024;
const VALID_UPLOADS_MIME_TYPES = ['image/jpeg', 'image/png'];

@Controller()
export class AppController {
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
public async uploadFile(
@UploadedFile(
new ParseFilePipeBuilder()
.addValidator(
new CustomUploadFileTypeValidator({
fileType: VALID_UPLOADS_MIME_TYPES,
}),
)
.addMaxSizeValidator({ maxSize: MAX_PROFILE_PICTURE_SIZE_IN_BYTES })
.build({ errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY }),
)
file,
) {
return "file upload successful";
}
}

We have imported the CustomUploadFileTypeValidator class and initialized it with an array of mime types that our application considers valid. With this is in place, only files that are 2 MBs or smaller in size and are of type jpeg or png will be accepted as valid upload types. Should we wish to configure our acceptable upload types, we can do so by altering the VALID_UPLOADS_MIME_TYPES array.

Conclusion

Albeit a little tedious to put together, uploading files in NestJS is pretty simple as the framework already provides blocks to help us do so and all we’re left to do is to piece the blocks together to complete the puzzle.

--

--

Responses (2)