Logo

dev-resources.site

for different kinds of informations.

Hybrid NestJs Microservice Responding to Both HTTP and gRPC Requests

Published at
9/8/2024
Categories
nestjs
grpc
microservices
nx
Author
ivargb
Categories
4 categories in total
nestjs
open
grpc
open
microservices
open
nx
open
Author
6 person written this
ivargb
open
Hybrid NestJs Microservice Responding to Both HTTP and gRPC Requests

Image description

This post is not intended to compare the pros and cons of gRPC versus REST. Instead, the focus is on how to combine both.

In some cases, you may need your microservice to communicate with a browser via REST, while also allowing internal microservices to communicate with it. For internal communication, gRPC is often the better choice due to its speed and language-agnostic capabilities.

File Structure and Overview

To handle both REST and gRPC, we’ll need two controllers—one for each protocol—both communicating with a shared service. The REST setup is straightforward, but gRPC requires a few additional files in the libs folder, which stores shared resources across the monorepo. The libs folder is located at the root of the project, while the microservice itself is placed in the apps folder.

Setting up the REST Controller

To generate the microservice, use the following Nx command: nx g @nx/nest:application hybrid-app. Afterward, rename the generated controller to http-hybrid-app.controller.ts. Below is an example of the file’s contents:


import { Body, Controller, Post, UseFilters } from '@nestjs/common';
import { HybridAppService } from './hybrid-app.service';
import { CustomExceptionFilter } from '@monorepo/utils';

@Controller()
@UseFilters(CustomExceptionFilter)
export class HttpHybridAppController {
  constructor(private readonly hybridAppService: HybridAppService) {}

  @Post('greet')
  public async greet(@Body() dto: { **some type** }) {
    return this.hybridAppService.greetTheUser({ ...dto });
  }

  @Post('meet')
  public async meet(@Body() dto: { **some type** }) {
    return this.hybridAppService.meetTheUser({ ...dto });
  }
}

Enter fullscreen mode Exit fullscreen mode

This controller handles REST requests and communicates with a shared service to process the logic. The @UseFilters decorator applies a custom exception filter to ensure consistent error handling. This setup is intentional, as it later allows us to demonstrate how error handling differs when using gRPC.

Setting up the gRPC Controller

Before setting up your gRPC controller, you first need to create a .proto file, which defines the structure of the gRPC service.

I’ve placed my .proto files in the libs/proto folder. This organization keeps the files accessible as shared resources across the monorepo. If you decide to extend this example by creating a client gRPC microservice to communicate with the hybrid service, both services will need to use the same .proto file definition, making it convenient to store it in a shared location.

It is expected that you have at least basic knowledge in protobuf before diving in the next step, which is the content of the hybrid.proto file:

syntax = "proto3";

package hybrid;

message GreetDto {
  string greeting = 1;
  string full_name = 2;
}

message GreetResponse {
  string greet = 1;
}

message MeetDto {
  string name = 1;
  string surname = 2;
  int32 age = 3;
}

message MeetResponse {
  string meet = 1;
}

service HybridAppService {
  rpc Greet (GreetDto) returns (GreetResponse);
  rpc Meet (MeetDto) returns (MeetResponse);
}
Enter fullscreen mode Exit fullscreen mode

While this format may seem unfamiliar, it can be converted into readable TypeScript code for use in your microservice. To do this, you need to install the Google protobuf compiler. This tool provides the protoc command, which you can run to generate the TypeScript file:

protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./ --ts_proto_opt=nestJs=true ./libs/proto/hybrid.proto
Enter fullscreen mode Exit fullscreen mode

This command will generate a .ts file in the same directory as hybrid.proto (my practice is to move the file under libs/types). The resulting file looks like this:

// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
//   protoc-gen-ts_proto  v2.0.4
//   protoc               v5.27.3
// source: shared-resources/proto/hybrid.proto

/* eslint-disable */
import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices";
import { Observable } from "rxjs";

export const protobufPackage = "hybrid";

export interface GreetDto {
  greeting: string;
  fullName: string;
}

export interface GreetResponse {
  greet: string;
}

export interface MeetDto {
  name: string;
  surname: string;
  age: number;
}

export interface MeetResponse {
  meet: string;
}

export const HYBRID_PACKAGE_NAME = "hybrid";

export interface HybridAppServiceClient {
  greet(request: GreetDto): Observable<GreetResponse>;

  meet(request: MeetDto): Observable<MeetResponse>;
}

export interface HybridAppServiceController {
  greet(request: GreetDto): Promise<GreetResponse> | Observable<GreetResponse> | GreetResponse;

  meet(request: MeetDto): Promise<MeetResponse> | Observable<MeetResponse> | MeetResponse;
}

export function HybridAppServiceControllerMethods() {
  return function (constructor: Function) {
    const grpcMethods: string[] = ["greet", "meet"];
    for (const method of grpcMethods) {
      const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method);
      GrpcMethod("HybridAppService", method)(constructor.prototype[method], method, descriptor);
    }
    const grpcStreamMethods: string[] = [];
    for (const method of grpcStreamMethods) {
      const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method);
      GrpcStreamMethod("HybridAppService", method)(constructor.prototype[method], method, descriptor);
    }
  };
}

export const HYBRID_APP_SERVICE_NAME = "HybridAppService";
Enter fullscreen mode Exit fullscreen mode

Below is an example of how to structure your gRPC controller to utilize the TypeScript definitions.

import { Controller, UseFilters } from '@nestjs/common';
import { HybridAppService } from './hybrid-app.service';
import { RpcCustomExceptionFilter } from '@monorepo/utils';
import { GrpcMethod } from '@nestjs/microservices';
import { HybridAppServiceController, HybridAppServiceControllerMethods, HYBRID_APP_SERVICE_NAME } from '@monorepo/types';

@Controller()
@UseFilters(RpcCustomExceptionFilter)
@HybridAppServiceControllerMethods()
export class GrpcHybridAppController implements HybridAppServiceController {
  constructor(private readonly hybridAppService: HybridAppService) {}

  public async greet(dto: { greeting: string; fullName: string }) {
    return this.hybridAppService.greetTheUser(dto);
  }

  public async meet(dto: { name: string; surname: string; age: number }) {
    return this.hybridAppService.meetTheUser(dto);
  }
}

Enter fullscreen mode Exit fullscreen mode
  • HybridAppServiceController is an interface that enforces structure on your gRPC controller, ensuring it implements the necessary methods (Greet and Meet).

  • HybridAppServiceControllerMethods is a decorator that auto-implements boilerplate methods or configurations for the controller, reducing manual setup.

  • GrpcMethod binds a method in your NestJS controller to a specific gRPC method defined in the .proto file.

The final step is connecting your gRPC microservice during application bootstrapping. This is straightforward and can be done using NestJS’s hybrid application support:

import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { AppModule } from './app/app.module';
import { HYBRID_PACKAGE_NAME } from '@monorepo/types';
import { join } from 'path';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Connect the gRPC microservice
  await app.connectMicroservice({
    transport: Transport.GRPC,
    options: {
      port: '5000',
      protoPath: join(__dirname, '../../libs/proto/hybrid.proto'),
      package: HYBRID_PACKAGE_NAME, // Package name generated from the proto file
      loader: {
        keepCase: true,
      },
    },
  });

  await app.startAllMicroservices();
  await app.listen(3000);
}

bootstrap();
Enter fullscreen mode Exit fullscreen mode

Lets not forget the custom error handling of these requests. gRPC relies on status codes and metadata to convey details about errors. Let’s look at an example:

import { ArgumentsHost, Catch } from '@nestjs/common';
import { BaseRpcExceptionFilter } from '@nestjs/microservices';
import { Metadata, StatusBuilder, StatusObject } from '@grpc/grpc-js';
import { Status } from '@grpc/grpc-js/build/src/constants';
import { Observable, throwError } from 'rxjs';

// Custom validation exception class
class ValidationException extends Error {
  constructor(public errors: Record<string, string[]>) {
    super('Validation Error');
  }
}

@Catch(ValidationException)
export class RpcValidationExceptionFilter extends BaseRpcExceptionFilter {
  catch(exception: ValidationException, host: ArgumentsHost): Observable<StatusObject> {
    const metadata = new Metadata();
    metadata.add('errors', JSON.stringify(exception.errors));

    const statusBuilder = new StatusBuilder();
    const statusObject = statusBuilder
      .withCode(Status.INVALID_ARGUMENT) 
      .withDetails('Validation failed') 
      .withMetadata(metadata) 
      .build();

    return throwError(() => statusObject);
  }
}
Enter fullscreen mode Exit fullscreen mode

And there you have it — your hybrid application is now capable of handling both HTTP and gRPC requests, but also managing errors effectively.

Testing gRPC Endpoints

To test the gRPC endpoints, you can use Postman's gRPC client interface. It provides an easy way to interact with gRPC services.

Accessing hybrid.ts via @monorepo/types

If you’re wondering how I do this, it’s thanks to configuring the paths option in the root tsconfig.base.json file:

"compilerOptions": {
   "paths": {
         "@monorepo/libs": ["libs/types/index.ts"],
   }
}
Enter fullscreen mode Exit fullscreen mode

This allows TypeScript to resolve the path for shared code across the monorepo.

grpc Article's
30 articles in total
Favicon
How to Handle Excessive Warning Messages When Running `pecl install grpc`
Favicon
Protocol Buffers as a Serialization Format
Favicon
gRPC, Haskell, Nix, love, hate
Favicon
🚀Build, Implement, and Test gRPC Services with .NET9
Favicon
Using OpenTelemetry with gRPC in Node.js and Express Hybrid Applications
Favicon
Exploring gRPC: The High-Performance Communication in Distributed Systems
Favicon
Wednesday Links - Edition 2024-11-06
Favicon
Part 3: Compiling the Protos and Setting up the gRPC server
Favicon
Mocking gRPC Clients in C#: Fake It Till You Make It
Favicon
How gRPC Works
Favicon
Part 2: Defining the Authentication gRPC Interface
Favicon
How Does gRPC Actually Work?
Favicon
gRPC Streaming: Best Practices and Performance Insights
Favicon
Why is gRPC so much faster than a JSON-based REST API?
Favicon
Strongly typed web APIs with gRPC
Favicon
Use RBAC to protect your gRPC service right on proto definition
Favicon
gRPC and Go: Building High-Performance Web Services
Favicon
Connecting NestJS and ASP.NET Core with gRPC: A Step-by-Step Guide
Favicon
gRPC: what is it? An introduction...
Favicon
gRPC: onde vive? o que come?
Favicon
Understanding the Importance of gRPC and Its Applications in Modern Software Development
Favicon
Mastering Go gRPC Services with Docker: A One-Stop Guide
Favicon
Hybrid NestJs Microservice Responding to Both HTTP and gRPC Requests
Favicon
Part 1: How to build Auth API in Rust using gRPC
Favicon
gRPC between Web and Server.
Favicon
FauxRPC
Favicon
Why should we use Protobuf in Web API as data transfer protocol.
Favicon
Browser Client to gRPC Server Routing options: Connect, gRPC-web, gRPC-gateway and more!
Favicon
gRPC vs REST: A Comprehensive Comparison
Favicon
gRPC - Unimplemented Error 12

Featured ones: