DEV Community

Cover image for NestJS Upload File with FastifyAdapter
Rom858
Rom858

Posted on

NestJS Upload File with FastifyAdapter

Today I'm gonna implement a fastify-multer upload image for NestJS using FastifyAdapter(),

Look! is it possible?

Alt Text

In Document Official Page

WARNING
Multer cannot process data which is not in the supported multipart format (multipart/form-data). Also, note that this package is not compatible with the FastifyAdapter.

Create Project and install package

1- nest new fastify-file-upload
2- yarn add @nestjs/platform-fastify @nestjs/swagger fastify-multer fastify-swagger fastify-helmet
Enter fullscreen mode Exit fullscreen mode

main.ts

import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
import { contentParser } from 'fastify-multer';
import 'reflect-metadata';
import { join } from 'path';
import helmet from 'fastify-helmet';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
const swaggerDocument = new DocumentBuilder()
  .setTitle('API')
  .setDescription('API')
  .setVersion('1.0')
  .addTag('API')
  .build();
async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
  );
  await app.listen(3000);
  app.register(helmet, {
    contentSecurityPolicy: {
      directives: {
        defaultSrc: [`'self'`],
        styleSrc: [`'self'`, `'unsafe-inline'`],
        imgSrc: [`'self'`, 'data:', 'validator.swagger.io'],
        scriptSrc: [`'self'`, `https: 'unsafe-inline'`],
      },
    },
  });
  app.register(contentParser);
  app.useStaticAssets({ root: join(__dirname, '../../fastify-file-upload') });

  SwaggerModule.setup(
    'api',
    app,
    SwaggerModule.createDocument(app, swaggerDocument),
  );
  console.log(`APP IS RUNNING ON PORT ${await app.getUrl()}`);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

we need to set up helmet for fastify because FastifyAdapter is not compatible with Swagger and contentParser for upload file
and register static file when we want to preview image in browser
app.useStaticAssets({ root: join(__dirname, '../../fastify-file-upload') });

Note fastify-file-upload is your folder project

import { contentParser } from 'fastify-multer';
import helmet from 'fastify-helmet';

now we start code

fastify-file-interceptor.ts single file

import {
  CallHandler,
  ExecutionContext,
  Inject,
  mixin,
  NestInterceptor,
  Optional,
  Type,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import FastifyMulter from 'fastify-multer';
import { Options, Multer } from 'multer';

type MulterInstance = any;
export function FastifyFileInterceptor(
  fieldName: string,
  localOptions: Options,
): Type<NestInterceptor> {
  class MixinInterceptor implements NestInterceptor {
    protected multer: MulterInstance;

    constructor(
      @Optional()
      @Inject('MULTER_MODULE_OPTIONS')
      options: Multer,
    ) {
      this.multer = (FastifyMulter as any)({ ...options, ...localOptions });
    }

    async intercept(
      context: ExecutionContext,
      next: CallHandler,
    ): Promise<Observable<any>> {
      const ctx = context.switchToHttp();

      await new Promise<void>((resolve, reject) =>
        this.multer.single(fieldName)(
          ctx.getRequest(),
          ctx.getResponse(),
          (error: any) => {
            if (error) {
              // const error = transformException(err);
              return reject(error);
            }
            resolve();
          },
        ),
      );

      return next.handle();
    }
  }
  const Interceptor = mixin(MixinInterceptor);
  return Interceptor as Type<NestInterceptor>;
}
Enter fullscreen mode Exit fullscreen mode

fastify-files-interceptor.ts for multiple file

import {
  CallHandler,
  ExecutionContext,
  Inject,
  mixin,
  NestInterceptor,
  Optional,
  Type,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import FastifyMulter from 'fastify-multer';
import { Options, Multer } from 'multer';

type MulterInstance = any;
export function FastifyFilesInterceptor(
  fieldName: string,
  maxCount?: number,
  localOptions?: Options,
): Type<NestInterceptor> {
  class MixinInterceptor implements NestInterceptor {
    protected multer: MulterInstance;

    constructor(
      @Optional()
      @Inject('MULTER_MODULE_OPTIONS')
      options: Multer,
    ) {
      this.multer = (FastifyMulter as any)({ ...options, ...localOptions });
    }

    async intercept(
      context: ExecutionContext,
      next: CallHandler,
    ): Promise<Observable<any>> {
      const ctx = context.switchToHttp();

      await new Promise<void>((resolve, reject) =>
        this.multer.array(fieldName, maxCount)(
          ctx.getRequest(),
          ctx.getResponse(),
          (error: any) => {
            if (error) {
              // const error = transformException(err);
              return reject(error);
            }
            resolve();
          },
        ),
      );

      return next.handle();
    }
  }
  const Interceptor = mixin(MixinInterceptor);
  return Interceptor as Type<NestInterceptor>;
}
Enter fullscreen mode Exit fullscreen mode

Now go to app.controller.ts add following code

import {
  Body,
  Controller,
  Get,
  Post,
  Req,
  UploadedFile,
  UploadedFiles,
  UseInterceptors,
} from '@nestjs/common';
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { diskStorage } from 'multer';
import { AppService } from './app.service';
import { MultipleFileDto } from './dto/multiple-files-dto';
import { SingleFileDto } from './dto/single-file-dto';
import { FastifyFileInterceptor } from './interceptor/fastify-file-interceptor';
import { FastifyFilesInterceptor } from './interceptor/fastify-files-interceptor';
import { fileMapper, filesMapper } from './utils/file-mapper';
import { editFileName, imageFileFilter } from './utils/file-upload-util';
@Controller()
@ApiTags('Upload File ')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @ApiConsumes('multipart/form-data')
  @Post('single-file')
  @UseInterceptors(
    FastifyFileInterceptor('photo_url', {
      storage: diskStorage({
        destination: './upload/single',
        filename: editFileName,
      }),
      fileFilter: imageFileFilter,
    }),
  )
  single(
    @Req() req: Request,
    @UploadedFile() file: Express.Multer.File,
    @Body() body: SingleFileDto,
  ) {
    return { ...body, photo_url: fileMapper({ file, req }) };
  }

  @ApiConsumes('multipart/form-data')
  @Post('multiple-file')
  @UseInterceptors(
    FastifyFilesInterceptor('photo_url', 10, {
      storage: diskStorage({
        destination: './upload/single',
        filename: editFileName,
      }),
      fileFilter: imageFileFilter,
    }),
  )
  multiple(
    @Req() req: Request,
    @UploadedFiles() files: Express.Multer.File[],
    @Body() body: MultipleFileDto,
  ) {
    return { ...body, photo_url: filesMapper({ files, req }) };
  }
}
Enter fullscreen mode Exit fullscreen mode

single-file-dto.ts

import { ApiProperty } from "@nestjs/swagger";

// you can add validate using class-validator
export class SingleFileDto{
    @ApiProperty({type:"string",format:"binary"})
    photo_url:string

    @ApiProperty({example:"Rom"})
    username:string

    @ApiProperty({example:"12345678"})
    password:string
}
Enter fullscreen mode Exit fullscreen mode

multiple-files-dto.ts

import { ApiProperty } from '@nestjs/swagger';

// you can add validate using class-validator
export class MultipleFileDto {
  @ApiProperty({ type: Array, format: 'binary' })
  photo_url: string[];

  @ApiProperty({ example: 'Rom' })
  username: string;

  @ApiProperty({ example: '12345678' })
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

Now create folder call utils

create file-upload-util.ts

import { Request } from 'express';
import { extname } from 'path';

export const editFileName = (
  req: Request,
  file: Express.Multer.File,
  callback
) => {
  const name = file.originalname.split('.')[0];
  const fileExtName = extname(file.originalname);
  const randomName = Array(4)
    .fill(null)
    .map(() => Math.round(Math.random() * 16).toString(16))
    .join('');
  callback(null, `${name}-${randomName}${fileExtName}`);
};

export const imageFileFilter = (
  req: Request,
  file: Express.Multer.File,
  callback
) => {
  if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) {
    return callback(new Error('Only image files are allowed!'), false);
  }
  callback(null, true);
};
Enter fullscreen mode Exit fullscreen mode

create file-mappter.ts

import { Request } from 'express';

interface FileMapper {
  file: Express.Multer.File;
  req: Request;
}

interface FilesMapper {
  files: Express.Multer.File[];
  req: Request;
}

export const fileMapper = ({ file, req }: FileMapper) => {
  const image_url = `${req.protocol}://${req.headers.host}/${file.path}`;
  return {
    originalname: file.originalname,
    filename: file.filename,
    image_url,
  };
};

export const filesMapper = ({ files, req }: FilesMapper) => {
  return files.map((file) => {
    const image_url = `${req.protocol}://${req.headers.host}/${file.path}`;
    return {
      originalname: file.originalname,
      filename: file.filename,
      image_url,
    };
  });
};
Enter fullscreen mode Exit fullscreen mode

-Why define Interface Express for
@UploadFile() file:Express.Multer.File and
@UploadFiles() files:Express.Multer.File[] because the package we installed yarn add fastify-multer is base on the package express multer.

-file-upload-utils.ts have 2 function for editFilename and filterImage.

-file-mapper.ts for creating a new object.

-Now open chrome enter http://localhost:3000/api and try to upload have single file and multiple files.

Let's start upload our image on swagger ui

Single Image
Alt Text

Alt Text

Mutiple Image
Alt Text

Alt Text

You copy the image_url paste in the browser to view image

-Package Fastify-Multer Repository on GitHub: https://github.com/fox1t/fastify-multer

-Repository Code: https://github.com/chanphiromsok/Fastify-Upload

Top comments (4)

Collapse
 
muhammedalihudaybergenow profile image
Muha_king

How implement interceptor if client want to attache files with different keys

Collapse
 
rom858 profile image
Rom858

may you check this one npmjs.com/package/fastify-file-int...

Collapse
 
muhammedalihudaybergenow profile image
Muha_king

how to implement files interceptors if client want to attache files with different keys in to one route handler

Collapse
 
musilix profile image
Kareem

what a horrific amount of code we have to build out just to upload a file. there's gotta be a simpler way. not using fastify entirely, perhaps?