gRPC

gRPC is a modern, open source, high performance RPC framework that can run in any environment. It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication.

Like many RPC systems, gRPC is based on the concept of defining a service in terms of functions (methods) that can be called remotely. For each method, you define the parameters and return types. Services, parameters, and return types are defined in .proto files using Google's open source language-neutral protocol buffers mechanism.

With the gRPC transporter, Nest uses .proto files to dynamically bind clients and servers to make it easy to implement remote procedure calls, automatically serializing and deserializing structured data.

Installation#

To start building gRPC-based microservices, first install the required packages:


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

Overview#

Like other Nest microservices transport layer implementations, you select the gRPC transporter mechanism using the transport property of the options object passed to the createMicroservice() method. In the following example, we'll set up a hero service. The options property provides metadata about that service; its properties are described below.

main.ts
JS TS

const app = await NestFactory.createMicroservice(AppModule, {
  transport: Transport.GRPC,
  options: {
    package: 'hero',
    protoPath: join(__dirname, 'hero/hero.proto'),
  },
});
Hint The join() function is imported from the path package; the Transport enum is imported from the @nestjs/microservices package.

Options#

The gRPC transporter options object exposes the properties described below.

packageProtobuf package name (matches package setting from .proto file). Required
protoPath Absolute (or relative to the root dir) path to the .proto file. Required.
urlConnection url. String in the format ip address/dns name:port (for example, 'localhost:50051') defining the address/port on which the transporter establishes a connection. Optional. Defaults to 'localhost:5000'.
protoLoaderNPM package name for the utility to load .proto files. Optional. Defaults to '@grpc/proto-loader'.
loader@grpc/proto-loader options. These provide detailed control over the behavior of .proto files. Optional. See here for more details.
credentials Server credentials. Optional. Read more here.

Sample gRPC service#

Let's define our sample gRPC service called HeroService. In the above options object, theprotoPath property sets a path to the .proto definitions file hero.proto. The hero.proto file is structured using protocol buffers. Here's what it looks like:


// hero/hero.proto
syntax = "proto3";

package hero;

service HeroService {
  rpc FindOne (HeroById) returns (Hero) {}
}

message HeroById {
  int32 id = 1;
}

message Hero {
  int32 id = 1;
  string name = 2;
}

Our HeroService exposes a FindOne() method. This method expects an input argument of type HeroById and returns a Hero message (protocol buffers use message elements to define both parameter types and return types).

Next, we need to implement the service. To define a handler that fulfills this definition, we use the @GrpcMethod() decorator in a controller, as shown below. This decorator provides the metadata needed to declare a method as a gRPC service method.

Hint The @MessagePattern() decorator (read more) introduced in previous microservices chapters is not used with gRPC-based microservices. The @GrpcMethod() decorator effectively takes its place for gRPC-based microservices.
hero.controller.ts
JS TS

@Controller()
export class HeroController {
  @GrpcMethod('HeroService', 'FindOne')
  findOne(data: HeroById, metadata: any): Hero {
    const items = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Doe' },
    ];
    return items.find(({ id }) => id === data.id);
  }
}

@Controller()
export class HeroController {
  @GrpcMethod('HeroService', 'FindOne')
  findOne(data, metadata) {
    const items = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Doe' },
    ];
    return items.find(({ id }) => id === data.id);
  }
}
Hint The @GrpcMethod() decorator is imported from the @nestjs/microservices package.

The decorator shown above takes two arguments. The first is the service name (e.g., 'HeroService'), corresponding to the HeroService service definition in hero.proto. The second (the string 'FindOne') corresponds to the FindOne() rpc method defined within HeroService in the hero.proto file.

The findOne() handler method takes two arguments, the data passed from the caller and metadata that stores gRPC request metadata.

Both @GrpcMethod() decorator arguments are optional. If called without the second argument (e.g., 'FindOne'), Nest will automatically associate the .proto file rpc method with the handler based on converting the handler name to upper camel case (e.g., the findOne handler is associated with the FindOne rpc call definition). This is shown below.

hero.controller.ts
JS TS

@Controller()
export class HeroController {
  @GrpcMethod('HeroService')
  findOne(data: HeroById, metadata: any): Hero {
    const items = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Doe' },
    ];
    return items.find(({ id }) => id === data.id);
  }
}

@Controller()
export class HeroController {
  @GrpcMethod('HeroService)
  findOne(data, metadata) {
    const items = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Doe' },
    ];
    return items.find(({ id }) => id === data.id);
  }
}

You can also omit the first @GrpcMethod() argument. In this case, Nest automatically associates the handler with the service definition from the proto definitions file based on the class name where the handler is defined. For example, in the following code, class HeroService associates its handler methods with the HeroService service definition in the hero.proto file based on the matching of the name 'HeroService'.

hero.controller.ts
JS TS

@Controller()
export class HeroService {
  @GrpcMethod()
  findOne(data: HeroById, metadata: any): Hero {
    const items = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Doe' },
    ];
    return items.find(({ id }) => id === data.id);
  }
}

@Controller()
export class HeroService {
  @GrpcMethod()
  findOne(data, metadata) {
    const items = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Doe' },
    ];
    return items.find(({ id }) => id === data.id);
  }
}

Client#

Nest applications can act as gRPC clients, consuming services defined in .proto files. You access remote services through a ClientGrpc object. You can obtain a ClientGrpc object in several ways.

The preferred technique is to import the ClientsModule. Use the register() method to bind a package of services defined in a .proto file to an injection token, and to configure the service. The name property is the injection token. For gRPC services, use transport: Transport.GRPC. The options property is an object with the same properties described above.


imports: [
  ClientsModule.register([
    {
      name: 'HERO_PACKAGE',
      transport: Transport.GRPC,
      options: {
        package: 'hero',
        protoPath: join(__dirname, 'hero/hero.proto'),
      },
    },
  ]),
];
Hint The register() method takes an array of objects. Register multiple packages by providing a comma separated list of registration objects.

Once registered, we can inject the configured ClientGrpc object with @Inject(). Then we use the ClientGrpc object's getService() method to retrieve the service instance, as shown below.


@Injectable()
export class AppService implements OnModuleInit {
  private heroService: HeroService;

  constructor(@Inject('HERO_PACKAGE') private readonly client: ClientGrpc) {}

  onModuleInit() {
    this.heroService = this.client.getService<HeroService>('HeroService');
  }

  getHero(): Observable<string> {
    return this.heroService.findOne({ id: 1 });
  }
}

Notice that there is a small difference compared to the technique used in other microservice transport methods. Instead of the ClientProxy class, we use the ClientGrpc class, which provides the getService() method. The getService() generic method takes a service name as an argument and returns its instance (if available).

Alternatively, you can use the @Client() decorator to instantiate a ClientGrpc object, as follows:


@Injectable()
export class AppService implements OnModuleInit {
  @Client({
    transport: Transport.GRPC,
    options: {
      package: 'hero',
      protoPath: join(__dirname, 'hero/hero.proto'),
    },
  })
  client: ClientGrpc;

  private heroService: HeroService;

  onModuleInit() {
    this.heroService = this.client.getService<HeroService>('HeroService');
  }

  getHero(): Observable<string> {
    return this.heroService.findOne({ id: 1 });
  }
}

Finally, for more complex scenarios, we can inject a dynamically configured client using the ClientProxyFactory class as described here.

In either case, we end up with a reference to our HeroService proxy object, which exposes the same set of methods that are defined inside the .proto file. Now, when we access this proxy object (i.e., heroService), the gRPC system automatically serializes requests, forwards them to the remote system, returns a response, and deserializes the response. Because gRPC shields us from these network communication details, heroService looks and acts like a local provider.

Note, all service methods are lower camel cased (in order to follow the natural convention of the language). So, for example, while our .proto file HeroService definition contains the FindOne() function, the heroService instance will provide the findOne() method.


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

A message handler is also able to return an Observable, in which case the result values will be emitted until the stream is completed.

hero.controller.ts
JS TS

@Get()
call(): Observable<any> {
  return this.heroService.findOne({ id: 1 });
}

@Get()
call() {
  return this.heroService.findOne({ id: 1 });
}

A full working example is available here.

gRPC Streaming#

gRPC on its own supports long-term live connections, conventionally known as streams. Streams are useful for cases such as Chatting, Observations or Chunk-data transfers. 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 write responses right inside of a Controller method or to be passed down to Subject/Observable consumer
  • Pure GRPC call stream handler: can be useful to be passed to some executor which will handle the rest of dispatch for the Node standard Duplex stream handler.

Subject strategy#

The @GrpcStreamMethod() decorator provides the function parameter as an RxJS Observable.


// Set decorator with selecting a Service definition from protobuf package
// the string is matching to: package proto_example.orders.OrdersService
@GrpcStreamMethod('orders.OrderService')
handleStream(messages: Observable<any>): Observable<any> {
  const subject = new Subject();
  messages.subscribe(message => {
    console.log(message);
    subject.next({
      shipmentType: {
        carrier: 'test-carrier',
      },
    });
  });
  return subject.asObservable();
}

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

Call stream handler#

The @GrpcStreamCall() decorator provides the function parameter as grpc.ServerDuplexStream, which supports standard methods like .on('data', callback), .write(message) or .cancel(). Full documentation on available methods can be found here.


// Set decorator with selecting a Service definition from protobuf package
// the string is matching to: package proto_example.orders.OrdersService
@GrpcStreamCall('orders.OrderService')
handleStream(stream: any) {
  stream.on('data', (msg: any) => {
    console.log(msg);
    // Answer here or anywhere else using stream reference
    stream.write({
      shipmentType: {
        carrier: 'test-carrier',
      },
    });
  });
}

This decorator does not require any specific return parameter to be provided. It is expected that the stream will be handled similar to any other standard stream type.

Support us

Nest is an MIT-licensed open source project. It can grow thanks to the support by these awesome people. If you'd like to join them, please read more here.

Principal Sponsor

Sponsors / Partners

Become a sponsor