NestJS: Creating a pipe to optimize uploaded images.
Anderson. J
Posted on March 23, 2022
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 have 2 common use cases:
- Validation
- 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
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],
})
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);
}
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
npm i -D @types/sharp
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> {
}
}
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;
}
}
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;
Then we create a new file name, to avoid duplicated collisions. Finally, we add the new extension: .webp
const filename = Date.now() + '-' + originalName + '.webp';
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));
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);
}
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
Posted on March 23, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.