Uploading Files to a NestJS Backend

In a recent tutorial, we covered how to upload files from a frontend application using <input type="file"> to a simple Node/Express server. In this tutorial, we will be walking through how to use NestJS as the backend server instead (NestJS actually sits on top of Node/Express).

I would recommend reading the previous tutorial for a lot more context on how uploading files to a server work. The previous tutorial covers what multipart/form-data is and how it relates to the forms we need to create to upload files, as well as using the FormData API, the role of multer and busboy and more.

You can also watch the video version of this tutorial below:

This tutorial will just be focusing on the basics of implementing the backend with NestJS. For reference, I will paste the code from the previous tutorial that we will be using to send files to our NestJS server below. The key parts are:

Listening for a the file change event:

<input type="file" onChange={ev => this.onFileChange(ev)}></input>

Storing a reference to the file for use later:

onFileChange(fileChangeEvent) {
  this.file = fileChangeEvent.target.files[0];
}

Constructing the FormData that will be sent with POST request to the NestJS server:

let formData = new FormData();
formData.append('photo', this.file, this.file.name);

The example frontend code below was created with StencilJS, but you will find example for Angular and React in the previous tutorial if you wish. The concept is more or less the same regardless, so if you are comfortable with your chosen framework it should be relatively straightforward to translate these concepts into your framework.

Frontend Code for Single File Upload

import { Component, h } from '@stencil/core';

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css',
})
export class AppHome {
  
  private file: File;

  onFileChange(fileChangeEvent) {
    this.file = fileChangeEvent.target.files[0];
  }

  async submitForm() {
    let formData = new FormData();
    formData.append('photo', this.file, this.file.name);

    try {
      const response = await fetch('http://localhost:3000/photos/upload', {
        method: 'POST',
        body: formData,
      });

      if (!response.ok) {
        throw new Error(response.statusText);
      }

      console.log(response);
    } catch (err) {
      console.log(err);
    }
  }

  render() {
    return [
      <ion-header>
        <ion-toolbar color="primary">
          <ion-title>Home</ion-title>
        </ion-toolbar>
      </ion-header>,

      <ion-content class="ion-padding">
        <ion-item>
          <ion-label>Image</ion-label>
          <input type="file" onChange={ev => this.onFileChange(ev)}></input>
        </ion-item>
        <ion-button color="primary" expand="full" onClick={() => this.submitForm()}>
          Upload Single
        </ion-button>
      </ion-content>,
    ];
  }
}

Frontend Code for Multiple File Upload

import { Component, h } from '@stencil/core';

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css',
})
export class AppHome {

  private fileOne: File;
  private fileTwo: File;
  private fileThree: File;

  onFileOneChange(fileChangeEvent) {
    this.fileOne = fileChangeEvent.target.files[0];
  }

  onFileTwoChange(fileChangeEvent) {
    this.fileTwo = fileChangeEvent.target.files[0];
  }

  onFileThreeChange(fileChangeEvent) {
    this.fileThree = fileChangeEvent.target.files[0];
  }

  async submitMultipleForm() {
    let formData = new FormData();
    formData.append('photos[]', this.fileOne, this.fileOne.name);
    formData.append('photos[]', this.fileTwo, this.fileTwo.name);
    formData.append('photos[]', this.fileThree, this.fileThree.name);

    try {
      const response = await fetch('http://localhost:3000/photos/uploads', {
        method: 'POST',
        body: formData,
      });

      if (!response.ok) {
        throw new Error(response.statusText);
      }

      console.log(response);
    } catch (err) {
      console.log(err);
    }
  }

  render() {
    return [
      <ion-header>
        <ion-toolbar color="primary">
          <ion-title>Home</ion-title>
        </ion-toolbar>
      </ion-header>,

      <ion-content class="ion-padding">
        <input type="file" onChange={ev => this.onFileOneChange(ev)}></input>
        <input type="file" onChange={ev => this.onFileTwoChange(ev)}></input>
        <input type="file" onChange={ev => this.onFileThreeChange(ev)}></input>
        <ion-button color="primary" expand="full" onClick={() => this.submitMultipleForm()}>
          Upload Multiple
        </ion-button>
      </ion-content>,
    ];
  }
}

1. Create the NestJS Project

We will start from scratch by creating a new NestJS project with the Nest CLI:

nest new

Although this is not strictly required for this example (you could just create an upload route in your applications main module) we will follow best practices and organise this application in modules. We will create a photos module using the nest g module command:

nest g module photos

We will also use the Nest CLI to generate a controller for us:

nest g controller photos

We will also call the enableCors method in the main.ts file so that our local frontend application can interact with the server:

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors();
  await app.listen(3000);
}
bootstrap();

2. Create the Routes

We will be creating two routes for this server: one to handle a single file upload and one to handle multiple file uploads.

import { Controller, Post} from "@nestjs/common";

@Controller("photos")
export class PhotosController {
  @Post("upload")
  uploadSingle() {

  }

  @Post("uploads")
  uploadMultiple() {

  }
}

Typically in a NestJS project we might define a DTO that would define the data that is being sent through a POST request, and then inject that DTO into the method for a particular route. Although you can POST other data along with your file(s) that will be available through the DTO, the files themselves are handled differently by NestJS.

3. Add File Interceptors

NestJS has built in functionality to intercept any files being uploaded and use multer to handle what to do with them. We can use FileInterceptor to intercept a file and then the @UploadedFile decorator to get a reference to the file that is being uploaded inside of our route handler method. In the case of multiple files being uploaded we can use FilesInterceptor and @UploadedFiles:

import { Controller, Post, UseInterceptors, UploadedFile, UploadedFiles } from "@nestjs/common";
import { FileInterceptor, FilesInterceptor } from "@nestjs/platform-express";

@Controller("photos")
export class PhotosController {
  @Post("upload")
  @UseInterceptors(FileInterceptor("photo", { dest: "./uploads" }))
  uploadSingle(@UploadedFile() file) {
    console.log(file);
  }

  @Post("uploads")
  @UseInterceptors(FilesInterceptor("photos[]", 10, { dest: "./uploads" }))
  uploadMultiple(@UploadedFiles() files) {
    console.log(files);
  }
}

All we need to do is specify the name of the field that contains the file(s) inside of File(s)Interceptor and then any multer options we want to use. As we did with the simple Node/Express example, we are just using a simple multer configuration:

{ dest: "./uploads" }

which will automatically save any uploaded files to the uploads directory. If this directory doesn’t already exist, it will automatically be created when you start your NestJS server.

The second parameter in the FilesInterceptor for multiple file uploads is a maxCount configuration, which in this case means no more then 10 files will be able to be uploaded at a time. Although we are just using a single multer configuration here by specifying the dest you can add any further multer options you want inside of this object.

Thanks for reading!

You can find the source code for the NestJS server, as well as the simple Node/Express server and the frontend code for StencilJS, Angular, and React below.

Check out my latest videos: