gRPC

The gRPC is a high-performance, open-source universal RPC framework.

Installation

Before we start, we have to install required package:

  1. $ npm i --save grpc @grpc/proto-loader

Transporter

In order to switch to gRPC transporter, we need to modify an options object passed to the createMicroservice() method.

  1. @@filename(main)
  2. const app = await NestFactory.createMicroservice(ApplicationModule, {
  3. transport: Transport.GRPC,
  4. options: {
  5. package: 'hero',
  6. protoPath: join(__dirname, 'hero/hero.proto'),
  7. },
  8. });

info Hint The join() function is imported from path package, while Transport enum is coming from @nestjs/microservices.

Options

There are a bunch of available options that determine a transporter behavior.

url Connection url
protoLoader NPM package name (if you want to use another proto-loader)
protoPath Absolute (or relative to the root dir) path to the .proto file
loader @grpc/proto-loader options. They are well-described here.
package Protobuf package name
credentials Server credentials (read more)

Overview

In general, a package property sets a protobuf package name, while protoPath is a path to the .proto definitions file. The hero.proto file is structured using protocol buffer language.

  1. syntax = "proto3";
  2. package hero;
  3. service HeroService {
  4. rpc FindOne (HeroById) returns (Hero) {}
  5. }
  6. message HeroById {
  7. int32 id = 1;
  8. }
  9. message Hero {
  10. int32 id = 1;
  11. string name = 2;
  12. }

In the above example, we defined a HeroService that exposes a FindOne() gRPC handler which expects HeroById as an input and returns a Hero message. In order to define a handler that fulfills this protobuf definition, we have to use a @GrpcMethod() decorator. The previously known @MessagePattern() is no longer useful.

  1. @@filename(hero.controller)
  2. @GrpcMethod('HeroService', 'FindOne')
  3. findOne(data: HeroById, metadata: any): Hero {
  4. const items = [
  5. { id: 1, name: 'John' },
  6. { id: 2, name: 'Doe' },
  7. ];
  8. return items.find(({ id }) => id === data.id);
  9. }
  10. @@switch
  11. @GrpcMethod('HeroService', 'FindOne')
  12. findOne(data, metadata) {
  13. const items = [
  14. { id: 1, name: 'John' },
  15. { id: 2, name: 'Doe' },
  16. ];
  17. return items.find(({ id }) => id === data.id);
  18. }

info Hint The @GrpcMethod() decorator is imported from @nestjs/microservices package.

The HeroService is a service name, while FindOne points to a FindOne() gRPC handler. The corresponding findOne() method takes two arguments, the data passed from the caller and metadata that stores gRPC request’s metadata.

Furthermore, the FindOne is actually redundant here. If you don’t pass a second argument to the @GrpcMethod(), Nest will automatically use the method name with the capitalized first letter, for example, findOne -> FindOne.

  1. @@filename(hero.controller)
  2. @Controller()
  3. export class HeroService {
  4. @GrpcMethod()
  5. findOne(data: HeroById, metadata: any): Hero {
  6. const items = [
  7. { id: 1, name: 'John' },
  8. { id: 2, name: 'Doe' },
  9. ];
  10. return items.find(({ id }) => id === data.id);
  11. }
  12. }
  13. @@switch
  14. @Controller()
  15. export class HeroService {
  16. @GrpcMethod()
  17. findOne(data, metadata) {
  18. const items = [
  19. { id: 1, name: 'John' },
  20. { id: 2, name: 'Doe' },
  21. ];
  22. return items.find(({ id }) => id === data.id);
  23. }
  24. }

Likewise, you might not pass any argument. In this case, Nest would use a class name.

  1. @@filename(hero.controller)
  2. @Controller()
  3. export class HeroService {
  4. @GrpcMethod()
  5. findOne(data: HeroById, metadata: any): Hero {
  6. const items = [
  7. { id: 1, name: 'John' },
  8. { id: 2, name: 'Doe' },
  9. ];
  10. return items.find(({ id }) => id === data.id);
  11. }
  12. }
  13. @@switch
  14. @Controller()
  15. export class HeroService {
  16. @GrpcMethod()
  17. findOne(data, metadata) {
  18. const items = [
  19. { id: 1, name: 'John' },
  20. { id: 2, name: 'Doe' },
  21. ];
  22. return items.find(({ id }) => id === data.id);
  23. }
  24. }

Client

In order to create a client instance, we need to use @Client() decorator.

  1. @Client({
  2. transport: Transport.GRPC,
  3. options: {
  4. package: 'hero',
  5. protoPath: join(__dirname, 'hero/hero.proto'),
  6. },
  7. })
  8. client: ClientGrpc;

There is a small difference compared to the previous examples. Instead of the ClientProxy class, we use the ClientGrpc that provides a getService() method. The getService() generic method takes service name as an argument and returns its instance if available.

  1. @@filename(hero.controller)
  2. onModuleInit() {
  3. this.heroService = this.client.getService<HeroService>('HeroService');
  4. }
  5. @@switch
  6. onModuleInit() {
  7. this.heroService = this.client.getService('HeroService');
  8. }

The heroService object exposes the same set of methods that have been defined inside .proto file. Note, all of them are lowercased (in order to follow the natural convention). Basically, our gRPC HeroService definition contains FindOne() function. It means that heroService instance will provide the findOne() method.

  1. interface HeroService {
  2. findOne(data: { id: number }): Observable<any>;
  3. }

All service methods return Observable. Since Nest supports RxJS streams and works pretty well with them, we can return them within HTTP handler as well.

  1. @@filename(hero.controller)
  2. @Get()
  3. call(): Observable<any> {
  4. return this.heroService.findOne({ id: 1 });
  5. }
  6. @@switch
  7. @Get()
  8. call() {
  9. return this.heroService.findOne({ id: 1 });
  10. }

A full working example is available here.

gRPC Streaming

GRPC on it’s own supports long-term live connections more known as streams.Streams can be a very useful instrument for such service cases as Chatting, Observationsor Chunk-data transfers. You can find more details in the official documentation (here).

Nest supports GRPC stream handlers in two possible ways:

  • RxJS Subject + Observable handler: can be useful to writeresponses right inside of a Controller method or to be passed downto Subject/Observable consumer
  • Pure GRPC call stream handler: can be useful to be passedto some executor which will handle the rest of dispatch forthe Node standard Duplex stream handler.

Subject strategy

@GrpcStreamMethod() decorator will provide the function parameter as RxJS Observable.

  1. // Set decorator with selecting a Service definition from protobuf package
  2. // the string is matching to: package proto_example.orders.OrdersService
  3. @GrpcStreamMethod('orders.OrderService')
  4. handleStream(messages: Observable<any>): Observable<any> {
  5. const subject = new Subject();
  6. messages.subscribe(message => {
  7. console.log(message);
  8. subject.next({
  9. shipmentType: {
  10. carrier: 'test-carrier',
  11. },
  12. });
  13. });
  14. return subject.asObservable();
  15. }

For support full-duplex interaction with @GrpcStreamMethod() decorator, it is required to return an RxJS Observablefrom the controller method.

Pure GRPC call stream handler

@GrpcStreamCall() decorator will provide function parameter as grpc.ServerDuplexStream, whichsupports standard methods like .on('data', callback), .write(message) or .cancel(),full documentation on available methods can be found here.

  1. // Set decorator with selecting a Service definition from protobuf package
  2. // the string is matching to: package proto_example.orders.OrdersService
  3. @GrpcStreamCall('orders.OrderService')
  4. handleStream(stream: any) {
  5. stream.on('data', (msg: any) => {
  6. console.log(msg);
  7. // Answer here or anywhere else using stream reference
  8. stream.write({
  9. shipmentType: {
  10. carrier: 'test-carrier',
  11. },
  12. });
  13. });
  14. }

This decorator do not require any specific return parameter to be provided.It is expected that stream will be handled in the way like any other standardstream type.