DEV Community

Cover image for NestJS: Creating a pipe to optimize uploaded images.
Anderson. J
Anderson. J

Posted on

NestJS: Creating a pipe to optimize uploaded images.

Cover image by Tim Mossholder on Pexels

Introduction.

In NestJS Context, pipes are intermediary between the incoming request and the request handled by the route handler.

Pipes to Handler diagram.

Pipes have 2 common use cases:

  1. Validation
  2. Transformation

In the case of transformation, pipes take care of transforming input data in a specific format to be received by the route handler.

An example of this would be converting a String to an Int, which is the case of ParseIntPipe.

In this post, we're going to build a pipe that takes an incoming image and transforms it into a size and web-friendly format.

Preparing Multer.

Nest uses by default Multer middleware to handle data sent with multipart/form-data which is used principally to upload files via HTTP POST.

First, we need to install Multer typings

npm i -D @types/multer
Enter fullscreen mode Exit fullscreen mode

Then let's import it to our root module.

// app.module.ts
@Module({
  // importing MulterModule and use memory storage to use the buffer within the pipe
  imports: [MulterModule.register({
    storage: memoryStorage()
  })],
  controllers: [AppController],
  providers: [AppService],
})
Enter fullscreen mode Exit fullscreen mode

The route handler needs to receive the uploaded file by the client, so we need to add the interceptor FileInterceptor() to extract the file from the request.

// app.controller.ts
  @Post()
  @UseInterceptors(FileInterceptor('image'))
  uploadImage(@UploadedFile() image: Express.Multer.File) {
    this.service.uploadImage(image);
  }
Enter fullscreen mode Exit fullscreen mode

Building the pipe.

To handle the image transformation we're going to use Sharp. Sharp is a high performance image processing module, it's very useful to convert large images to smaller formats.

Let's install this module along with its typings

npm install sharp
Enter fullscreen mode Exit fullscreen mode
npm i -D @types/sharp
Enter fullscreen mode Exit fullscreen mode

We can now create our pipe by creating the file sharp.pipe.ts

A pipe must implement the PipeTransform interface and must be annotated with the @Injectable() decorator.

PipeTransform<T, R> is a generic interface, where T is the input type and R is the type returned by the transform() method.

In this case, we expect to receive an Express.Multer.File and after the transform process, we're going to return a string with the name of the file.

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

@Injectable()
export class SharpPipe implements PipeTransform<Express.Multer.File, Promise<string>> {

  async transform(image: Express.Multer.File): Promise<string> {

  }

}
Enter fullscreen mode Exit fullscreen mode

With these lines, our pipe fulfills the PipeTransform interface. We're ready to begin the implementation.

The final code looks like this:

import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
import * as path from 'path';
import * as sharp from 'sharp';

@Injectable()
export class SharpPipe implements PipeTransform<Express.Multer.File, Promise<string>> {

  async transform(image: Express.Multer.File): Promise<string> {
    const originalName = path.parse(image.originalname).name;
    const filename = Date.now() + '-' + originalName + '.webp';

    await sharp(image.buffer)
      .resize(800)
      .webp({ effort: 3 })
      .toFile(path.join('uploads', filename));

    return filename;
  }

}
Enter fullscreen mode Exit fullscreen mode

Let's explain some of the lines from the above code.

image.originalname contains the file's original name, including its extension. We're planning to convert this file into a .WEBP file, so the original extension is not useful in this case. We only extract the file name with the path module.

const originalName = path.parse(image.originalname).name;
Enter fullscreen mode Exit fullscreen mode

Then we create a new file name, to avoid duplicated collisions. Finally, we add the new extension: .webp

const filename = Date.now() + '-' + originalName + '.webp';
Enter fullscreen mode Exit fullscreen mode

To finally convert our image, we execute sharp with the image buffer, we resize it to 800x800 and convert it to webp. Sharp has an extensive API to manipulate quality and sizes, you can find more options in their oficial docs

We finish by calling .toFile() with the path where this image is going to be saved. In this case it will be saved in ./uploads/<filename>.webp

await sharp(image.buffer)
  .resize(800)
  .webp({ effort: 3 })
  .toFile(path.join('uploads', filename));
Enter fullscreen mode Exit fullscreen mode

Our pipe is ready to be used, now we need to integrate it into our route handler. To do that, it's simple as passing our new pipe as an argument to the UploadedFile decorator.

As SharpPipe returns a string we need to change the image typing in the route handler. So we replace Express.Multer.File with string.

// app.controller.ts
  @Post()
  @UseInterceptors(FileInterceptor('image'))
  //                        vvv Our pipe
  uploadImage(@UploadedFile(SharpPipe) image: string) {
    this.service.uploadImage(image);
  }
Enter fullscreen mode Exit fullscreen mode

Conclusion.

And in that way, we already have a flow to optimize uploaded images by the client.

I made a quick test and tried to upload a 5MB image, after getting through the pipe the image size was 15.5KB. That's a decrease of ~ 99%!

If you liked this tutorial, please share and hit ❤

Further Reading

Top comments (5)

Collapse
 
riochndr profile image
Rio Chandra

Thank you very much !!

Collapse
 
rahulkirte profile image
rahulkirte

Thanks buddy. It really help to me but can you tell how to convert the size mb to kb.

Collapse
 
damuma profile image
damuma

Hi!! How to apply this pipe to multiple images upload?? Thanks a lot.

Collapse
 
vanaf25 profile image
vanaf25

how I can get the req in transform method?

Collapse
 
andersonjoseph profile image
Anderson. J

There's no a straightforward way to get the request object within pipes. You probably need to create a custom decorator for that purpose.

Here's an article that explains an approach to it: dev.to/avantar/injecting-request-o...