7 Steps to Resize and Optimize Images with Nest.js Using Transformation Pipes

GitHub repo: hal-efecan/image-resize-demo: A demo for resizing images using Nest.js and sharp (github.com)

Why would we want to resize user uploaded images in a web application?

Resizing images in a web application is a common practice for several important reasons:

Performance Optimization: Large images can significantly slow down web page load times. Resizing images to appropriate dimensions and file sizes help improve your web applications performance and allows for a better user experience.

Bandwidth Conservation: Sending large images over the internet consumes bandwidth, which can be costly for both users and the website owner, especially in cases of limited data plans or hosting expenses. Resizing images reduces the amount of data transferred.

Storage Efficiency: Storing large images takes up more server storage space. By resizing images to standard dimensions or using compression, you can reduce storage costs.

Consistent UI/UX: When users upload images of various sizes and dimensions, it can lead to layout issues or distorted visuals on the web page. Resizing images to fit specific design requirements helps maintain consistency.

Mobile Optimization: Mobile devices often have limited screen space and slower network connections. Resizing images for mobile devices ensures that users on smartphones and tablets receive appropriately sized images, improving load times and user experience on these devices.

Responsive Design: For responsive web design, where a website adapts to different screen sizes, resizing images for various breakpoints ensures that the website remains visually appealing on all devices.

So, as you can see there are many reasons why we should resize and optimize user uploaded images. With that said, let’s get started!

Step 1: Install Nest.js CLI

npm i -g @nestjs/cli

Step 2: Install type definitions for the Multer.

npm i -D @types/multer

what is Multer …

“Multer is a node.js middleware for handling multipart/form-data, which is primarily used for uploading files.”

Step 3: Install Sharp for image processing.

npm i sharp

what is Sharp …

“The typical use case for this high speed Node.js module is to convert large images in common formats to smaller, web-friendly JPEG, PNG, WebP, GIF and AVIF images of varying dimensions.

As well as image resizing, operations such as rotation, extraction, compositing and gamma correction are available.”

Step 4: Register the Multer Module in our app.module.ts

/* App.module.ts*/
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MulterModule } from '@nestjs/platform-express';

@Module({
  imports: [MulterModule.register()],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Step 5: Create a pipes folder in the src directory and create an image.pipe.ts (this is where all the magic will happen)

Your folder structure should look something like the below:


.
├── dist
├── node_modules
├── public/
│   └── images
├── src/
│   ├── pipes/
│   │   └── image.pipe.ts
│   ├── app.controller.ts
│   ├── app.service.ts
│   ├── app.module.ts
│   └── main.ts
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── nest-cli.json
├── package-lock.json
├── package.json
├── tsconfig.build.json
└── tsconfig.json

Step 6: Create our “upload” endpoint in the app.controller.ts

/* App.controller.ts*/
import {
  Controller,
  Post,
  UseInterceptors,
  UploadedFile,
} from '@nestjs/common';
import { AppService } from './app.service';
import { FileInterceptor } from '@nestjs/platform-express';

// Custom Pipe
import { ImagePipe } from './pipes/image.pipe';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post('upload')
  @UseInterceptors(FileInterceptor('image'))
  async uploadFile(@UploadedFile(ImagePipe) file: Express.Multer.File) {
    console.log('Your file has been saved to disk!');
    return file;
  }
}

Now let’s go over what is happening in the code snippet above:

we have created an AppController class which is a fundamental building block in Nest.js for handling HTTP requests and defining API endpoints.

  1. we created a @Post(“upload”) endpoint which is what we will be hitting when using Postman to upload and send the original image file.
  2. we imported the UseInterceptors decorator from “@nestjs/common” module which is used to specify middleware behavior for this endpoint
  3. we imported the FileInterceptor from “@nestjs/platform-express” which is used to handle file uploads. We then added “image” into the interceptor so that it can find the specific key we will be sending from Postman.
  4. The uploadFile method takes a parameter file of type Express.Multer.File. This parameter represents the uploaded file, and is processed by our custom ImagePipe before being passed to the body of our method.

In summary, this code defines a Nest.js controller with an endpoint for handling file uploads. It uses decorators to configure how the endpoint behaves, including specifying the middleware (FileInterceptor) for handling file uploads and applying a custom pipe (ImagePipe) to process the uploaded file before it reaches the uploadFile method.

Step 7: Now it’s time to create our custom ImagePipe which will implement the PipeTransform interface.


Before moving on it is important to understand what Pipes actually are in the first place and what purpose they actually serve.

A pipe is a class annotated with the @Injectable() decorator, which implements the PipeTransform interface.

pipes operate on the arguments being processed by a controller route handler. Nest interposes a pipe just before a method is invoked, and the pipe receives the arguments destined for the method and operates on them. Any transformation or validation operation takes place at that time, after which the route handler is invoked with any (potentially) transformed arguments.


/* transform.pipe.ts*/
import { Injectable, PipeTransform } from '@nestjs/common';
import { accessSync } from 'node:fs';
import { parse, join } from 'path';
import * as sharp from 'sharp';

@Injectable()
export class ImagePipe
  implements PipeTransform<Express.Multer.File, Promise<any>>
{
  async transform(image: Express.Multer.File): Promise<any> {
    const pathToSave = 'public/images';

    try {
      accessSync(pathToSave);
      const imageType = image.mimetype.split('/')[1];
      const originalName = parse(image.originalname).name;
      const filename = Date.now() + '-' + originalName + imageType;

      // Where the magic happens
      await sharp(image.buffer)
        .resize({
          width: 200,
          height: 200,
          fit: 'fill',
        })
        .toFile(join(pathToSave, filename));
      // Where the magic happens

      return filename;
    } catch (err) {
      console.error('Error', err);
    }
  }
}

Now let’s breakdown what is happening in the code above:

Imports
  1. We import theInjectable decorator and PipeTransform interface from @nestjs/common.
  2. We also import accessSync from node:fs we will use this to check if the public/images directory exists.
  3. path for path manipulation and join to join arguments and normalize the resulting path
  4. Finally we will import the sharp image processing library itself.
So what is actually happening?
  1. The ImagePipe class implements the PipeTransform interface, which is a generic interface provided by Nest for creating custom pipes. It specifies that this pipe takes an Express.Multer.File as an input and returns a Promise.
  2. The transform method itself is an asynchronous function that receives an image of type Express.Multer.File. This method processes the uploaded image and returns a filename (which is typically stored in a database and used to retrieve the optimized image that was saved to the disk).
  3. The constant pathToSave is the directory path where the processed image will be saved, which is public/images
  4. Within the try we have a check using accessSync if the public/images directory exists, it proceeds; otherwise, it catches any errors that occurs.
  5. We then extract the file extension from the mimetype property of the uploaded image. Then we constructs a unique filename by appending the current timestamp, the original name of the file (without extension), and the extracted file extension.
  6. The sharp library is then used to perform image processing. It resizes the image to a width and height of 200 pixels, with a 'fill' strategy, and saves it to the specified path with the constructed filename.

In summary, this ImagePipe class is designed to be used as a middleware in a Nest.js application to process and resize uploaded images and save them to a specific directory. It performs error handling and returns the generated filename if the processing is successful.

Now using Postman we can send a Post request by form-data and including “image” as a key and uploading an image from our local computer.

Loading image...

By clicking on send our API should now return the saved filename as well as well as having saving the image to our public/images directory.

Loading image...