Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

File uploads

Files are not sent as JSON but as multipart/form-data — the same way a browser submits a form with a file field. NestJS parses such requests with the Multer library, which is already part of the @nestjs/platform-express package. Let us add a cover image to posts.

Preparation

npm i -D @types/multer

The package adds the TypeScript type Express.Multer.File that describes an uploaded file. Create an uploads folder in the project root and add it to .gitignore.

The upload route

// src/posts/posts.controller.ts
import { Controller, Param, ParseIntPipe, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { extname } from 'node:path';
import { diskStorage } from 'multer';
 
@Controller('posts')
export class PostsController {
  @Post(':id/image')
  @UseInterceptors(
    FileInterceptor('image', {
      storage: diskStorage({
        destination: 'uploads',
        filename: (req, file, callback) => {
          callback(null, `${Date.now()}${extname(file.originalname)}`);
        },
      }),
    }),
  )
  uploadImage(
    @Param('id', ParseIntPipe) id: number,
    @UploadedFile() file: Express.Multer.File,
  ) {
    return { postId: id, filename: file.filename, size: file.size };
  }
}

FileInterceptor('image', ...) takes the form field named image and saves the file. diskStorage() sets the folder and the file name: the current time plus the original extension. The @UploadedFile() decorator gives the method information about the file.

Test the upload with curl — the -F flag sends a form, and @ before the name means "the file's contents":

curl -X POST http://localhost:3000/posts/1/image -F "image=@cover.jpg"

The server answers {"postId":1,"filename":"1767225600000.jpg","size":48213}, and the file appears in uploads. The file name is stored in a new post column, for example @Column({ nullable: true }) image: string.

Checking size and type

Without checks, anything of any size can be uploaded. The ParseFilePipe pipe takes a list of validators:

// src/posts/posts.controller.ts
import { FileTypeValidator, MaxFileSizeValidator, ParseFilePipe, UploadedFile } from '@nestjs/common';
 
uploadImage(
  @UploadedFile(
    new ParseFilePipe({
      validators: [
        new MaxFileSizeValidator({ maxSize: 2 * 1024 * 1024 }),
        new FileTypeValidator({ fileType: /image\/(jpeg|png|webp)/ }),
      ],
    }),
  )
  file: Express.Multer.File,
) {
  return { filename: file.filename };
}

A file over 2 MB or one that is not an image returns a 400 error. If no file is sent at all, the pipe also answers 400.

Serving uploaded files

To make images available by address, enable static files in src/main.ts:

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'node:path';
import { AppModule } from './app.module';
 
async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.useStaticAssets(join(process.cwd(), 'uploads'), { prefix: '/uploads/' });
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

The file now opens at http://localhost:3000/uploads/1767225600000.jpg.

Common mistake

The field name in FileInterceptor('image') does not match the form field name on the client, for example the client sends file. Multer then answers with an Unexpected field error, and without an interceptor file is simply undefined. The name in the interceptor and in the form must be the same.

Thanks for reading!

Donate to the site — we work hard for you.