gRPC, Nest.js image resize

February 7, 2024

Demonstrating NestJS and gRPC Integration Through an Image Cropping Application

In this project, we create a simple yet effective image cropping application leveraging the capabilities of NestJS and gRPC technologies. NestJS is a modern, comprehensive framework for building server-side applications with Node.js, while gRPC is a high-performance, open-source universal RPC framework that facilitates quick and efficient communication between services.

Install

Let's install nest.js

POWERSHELL
npm i -g @nestjs/cli

Next, let's create the project itself, which will serve as our API server. To create the project, we use the nest new command, followed by the name of the project, such as grpc-picture-resize. This command creates a new directory with the given name and initializes a new NestJS project within it:

POWERSHELL
nest new grpc-picture-resize

We will implement the logic for resizing images within a separate microservice, which allows us to isolate this functionality and easily scale it if necessary. Let's create this microservice using the nest g app command and add it to our project as a new application. For instance, we could name the microservice something like resize:

POWERSHELL
nest g app resize

For our system to function, we will also need to install the following libraries:

POWERSHELL
npm install @nestjs/microservices
POWERSHELL
npm install @grpc/proto-loader @grpc/grpc-js ts-proto

To crop images, we will install the following JavaScript library:

POWERSHELL
npm install sharp

Modify the nest-cli file according to our monorepo setup.

JSON
//nest-cli.json
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "apps/grpc-picture-resize/src",
"compilerOptions": {
"deleteOutDir": true,
"webpack": true,
"tsConfigPath": "apps/grpc-picture-resize/tsconfig.app.json"
},
"monorepo": true,
"root": "apps/grpc-picture-resize",
"projects": {
"grpc-picture-resize": {
"type": "application",
"root": "apps/grpc-picture-resize",
"entryFile": "src/main",
"sourceRoot": "./",
"compilerOptions": {
"tsConfigPath": "apps/grpc-picture-resize/tsconfig.app.json",
"assets": [
"proto/*.proto"
],
"watchAssets": true
}
},
"resize": {
"type": "application",
"root": "apps/resize",
"entryFile": "src/main",
"sourceRoot": "./",
"compilerOptions": {
"tsConfigPath": "apps/resize/tsconfig.app.json",
"assets": [
"proto/*.proto"
],
"watchAssets": true
}
}
}
}

Creating the resize.proto File and Converting It to TypeScript

The line package resize; defines the namespace for the protobuf messages and services. This helps to avoid name collisions when using different protobuf definitions together.

The service ImageResizer block defines a service that includes a ResizeImage RPC (Remote Procedure Call). This procedure expects a ResizeRequest and returns a ResizeResponse.

ResizeImage RPC: Allows clients to send an image for resizing. The request includes the image as a byte array (bytes image), along with the desired width (int32 width) and height (int32 height).

ResizeRequest: This message contains the data needed for resizing the image. The image field stores the binary content of the image, while the width and height fields specify the new dimensions.

ResizeResponse: The response message that includes the details of the resized image. fileName contains the name of the created image file, while resizedWidth and resizedHeight fields provide the width and height of the resized image.

PLAIN TEXT
//resize.prote
syntax = "proto3";
package resize;
service ImageResizer {
rpc ResizeImage (ResizeRequest) returns (ResizeResponse) {}
}
message ResizeRequest {
bytes image = 1;
int32 width = 2;
int32 height = 3;
}
message ResizeResponse {
string fileName = 1;
int32 resizedWidth = 2;
int32 resizedHeight = 3;
}

After creating the proto file, we use protoc to generate a TypeScript file from it.

On Windows, you can apply protoc with the following steps:

  • Download the latest version of protoc.
  • Install it using choco install protoc.
  • Finally, add the file's bin directory to your environmental variables.
  • From the main directory of our program, we run the following code:

    POWERSHELL
    protoc --plugin=protoc-gen-ts_proto="C:\path\to\your\project\folder\node_modules\.bin\protoc-gen-ts_proto.cmd" --ts_proto_out=./ --ts_proto_opt=nestJs=true ./proto/min.proto

    For the --plugin option, we specify the plugin used and its location.

    For the --ts_proto_out option, we indicate that we are optimizing for NestJS, where to copy the output, and the location of our proto file used.

    Api setting

    In the AppController class, we define a POST endpoint ('/resize'), which allows users to request image resizing by providing an image URL, along with the desired width and height. The method downloads the image from the provided URL, creates a resize request from the downloaded image, and then resizes the image using the resizeImage service call.

    TYPESCRIPT
    //grpc-picture-resize/src/app.controller.ts
    import { Body, Controller, Post } from '@nestjs/common';
    import { AppService } from './app.service';
    import fetch from 'node-fetch';
    @Controller()
    export class AppController {
    constructor(private readonly appService: AppService) {}
    @Post('/resize')
    async resizeImageByUrl(
    @Body() body: { imageUrl: string; width: number; height: number },
    ) {
    // Downloading the image from the provided URL
    const response = await fetch(body.imageUrl);
    if (!response.ok) {
    throw new Error('Failed to download the image');
    }
    const imageBuffer = await response.buffer();
    // Creating a resize request with the downloaded image
    const resizeRequest = {
    image: imageBuffer,
    width: body.width,
    height: body.height,
    };
    // Resizing the image using the gRPC service
    return this.appService.resizeImage(resizeRequest);
    }
    }

    In the AppModule file, we configure the gRPC client using the ClientsModule.register method. Here, we specify a service name ('resize') that matches the package name defined in the proto file and set the communication mode to Transport.GRPC. With the protoPath option, we provide the path to the gRPC definition file, which describes the interface for resizing images.

    TYPESCRIPT
    //grpc-picture-resize/src/app.module.ts
    import { Module } from '@nestjs/common';
    import { ClientsModule, Transport } from '@nestjs/microservices';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { join } from 'path';
    @Module({
    imports: [
    ClientsModule.register([
    {
    name: 'resize',
    transport: Transport.GRPC,
    options: {
    package: 'resize',
    protoPath: join(__dirname, '../resize.proto'),
    },
    },
    ]),
    ],
    controllers: [AppController],
    providers: [AppService],
    })
    export class AppModule {}

    The AppService class is responsible for consuming the gRPC service. We inject the gRPC client in the constructor, and in the onModuleInit method, we initialize the image resizing client-service using the getService<ImageResizerClient> call. The resizeImage method executes the image resizing request and returns the file name of the resized image.

    TYPESCRIPT
    //grpc-picture-resize/src/app.service.ts
    import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
    import { ClientGrpc } from '@nestjs/microservices';
    import {
    ResizeRequest,
    IMAGE_RESIZER_SERVICE_NAME,
    ImageResizerClient,
    } from 'proto/resize';
    @Injectable()
    export class AppService implements OnModuleInit {
    private imageResizerClient: ImageResizerClient;
    constructor(@Inject('resize') private clientGrpc: ClientGrpc) {}
    onModuleInit() {
    this.imageResizerClient = this.clientGrpc.getService<ImageResizerClient>(
    IMAGE_RESIZER_SERVICE_NAME,
    );
    }
    async resizeImage(resizeRequest: ResizeRequest) {
    const resizedImage = await this.imageResizerClient
    .resizeImage(resizeRequest)
    .toPromise();
    return resizedImage.fileName;
    }
    }

    Finally, in the main.ts file, we create and start the NestJS application. With the line await app.listen(3000);, the application listens on port 3000, allowing access to the API.

    TYPESCRIPT
    //grpc-picture-resize/src/main.ts
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    await app.listen(3000);
    }
    bootstrap();

    Image Resizing Solution

    The ResizeController is responsible for receiving and handling gRPC calls. The resizeImage method, marked with the @GrpcMethod() decorator, accepts ResizeImage calls from gRPC clients. In this method, the image resizing functionality provided by the ResizeService is invoked, which expects a ResizeRequest object as a parameter and returns with a ResizeResponse object.

    TYPESCRIPT
    //resize/src/resize.controller.ts
    import { Controller } from '@nestjs/common';
    import { GrpcMethod } from '@nestjs/microservices';
    import { ResizeService } from './resize.service';
    import { ResizeRequest, ResizeResponse } from 'proto/resize';
    @Controller()
    export class ResizeController {
    constructor(private readonly resizeService: ResizeService) {}
    @GrpcMethod('ImageResizer', 'ResizeImage')
    async resizeImage(resizeRequest: ResizeRequest): Promise<ResizeResponse> {
    return this.resizeService.resizeImage(resizeRequest);
    }
    }

    The ResizeModule is responsible for packaging the controller and service into a NestJS module. This module ensures that the controller and service are available and usable within the microservice.

    TYPESCRIPT
    //resize/src/resize.module.ts
    import { Module } from '@nestjs/common';
    import { ResizeService } from './resize.service';
    import { ResizeController } from './resize.controller';
    @Module({
    controllers: [ResizeController],
    providers: [ResizeService],
    })
    export class ResizeModule {}

    The ResizeService implements the actual image resizing using the sharp library. The resizeImage method accepts a ResizeRequest object, which includes the image data (in Buffer form), as well as the desired width and height. Using sharp, it resizes the image, then saves the generated image to disk in a predetermined directory. Finally, it returns a ResizeResponse object, which includes the name of the generated file, as well as the width and height of the resized image.

    TYPESCRIPT
    //resize/src/resize.service.ts
    import { Injectable } from '@nestjs/common';
    import * as sharp from 'sharp';
    import { ResizeRequest, ResizeResponse } from 'proto/resize';
    import * as fs from 'fs';
    import * as path from 'path';
    @Injectable()
    export class ResizeService {
    private saveDirectory = path.join(__dirname, '../');
    async resizeImage(resizeRequest: ResizeRequest): Promise<ResizeResponse> {
    try {
    const fileName = `resized-${Date.now()}.jpg`;
    const outputPath = path.join(this.saveDirectory, fileName);
    const { data: resizedImage, info } = await sharp(
    Buffer.from(resizeRequest.image),
    )
    .resize(resizeRequest.width, resizeRequest.height)
    .toBuffer({ resolveWithObject: true });
    // Save to disk
    fs.writeFileSync(outputPath, resizedImage);
    return {
    fileName: fileName,
    resizedWidth: info.width,
    resizedHeight: info.height,
    };
    } catch (error) {
    console.error('Image resizing and saving failed: ', error.message);
    throw new Error(error.message);
    }
    }
    }

    The bootstrap function starts the microservice. In this case, we use the NestFactory.createMicroservice method to create, configure, and start the gRPC server. The transport setting is set to Transport.GRPC, enabling NestJS to use the gRPC protocol for communication. In the options object, we specify the path to the proto file and the package name, which describe the interface and message formats used for communication.

    TYPESCRIPT
    //resize/src/main.ts
    import { NestFactory } from '@nestjs/core';
    import { MicroserviceOptions, Transport } from '@nestjs/microservices';
    import { ResizeModule } from './resize.module';
    import { join } from 'path';
    async function bootstrap() {
    const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    ResizeModule,
    {
    transport: Transport.GRPC,
    options: {
    package: 'resize',
    protoPath: join(__dirname, '../resize.proto'),
    },
    },
    );
    await app.listen();
    }
    bootstrap();

    Testing

    For simplicity, we only return the image file name. The modified image is saved locally, as specified in the code above.

    Starting the Microservice and Client Application: First, let's start both the image resizing microservice and the client application with the following commands:

  • Starting the image resizing microservice:
  • POWERSHELL
    npm run start:dev resize
  • Starting the client application:
  • POWERSHELL
    npm run start:dev grpc-picture-resize

    Testing with Postman or curl:

    To test the system, we can use either the Postman application or the curl command-line tool. For testing, send a POST request to the client application's /resize endpoint, specifying the image URL, desired width, and height in the request body.

    For example, using curl:

    POWERSHELL
    curl -X POST

    After successful image resizing, the image resized with the specified parameters will be saved in a local directory configured by the resize application. The image file name, which includes the timestamp of resizing, will be sent back to the requester, allowing for verification of the results.

    Next Steps

    Utilizing gRPC for image resizing offers several advantages, especially in microservices architectures or systems composed of multiple distinct components where communication is a key factor. One potential enhancement to the code could involve uploading the resized images to AWS S3 and then returning the URL of the uploaded image to the user.

    Code

    The complete code is available:

    https://github.com/balazsfaragodev/grpc-nestjs-tutorial-beginner

    Share this article

    The Newsletter for Next-Level Tech Learning

    Start your day right with the daily newsletter that entertains and informs. Subscribe now for free!

    Related Articles