NestJS: Creating a pipe to optimize uploaded images.

andersonjoseph

Anderson. J

Posted on March 23, 2022

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

💖 💪 🙅 🚩
andersonjoseph
Anderson. J

Posted on March 23, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related