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:

  1. $ 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

  1. const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
  2. transport: Transport.GRPC,
  3. options: {
  4. package: 'hero',
  5. protoPath: join(__dirname, 'hero/hero.proto'),
  6. },
  7. });
  1. const app = await NestFactory.createMicroservice(AppModule, {
  2. transport: Transport.GRPC,
  3. options: {
  4. package: 'hero',
  5. protoPath: join(__dirname, 'hero/hero.proto'),
  6. },
  7. });

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
protoPathAbsolute (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
credentialsServer credentials. Optional. Read more here

Sample gRPC service

Let’s define our sample gRPC service called HeroesService. 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:

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

Our HeroesService 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.

heroes.controller.ts

  1. @Controller()
  2. export class HeroesController {
  3. @GrpcMethod('HeroesService', 'FindOne')
  4. findOne(data: HeroById, metadata: any): Hero {
  5. const items = [
  6. { id: 1, name: 'John' },
  7. { id: 2, name: 'Doe' },
  8. ];
  9. return items.find(({ id }) => id === data.id);
  10. }
  11. }
  1. @Controller()
  2. export class HeroesController {
  3. @GrpcMethod('HeroesService', 'FindOne')
  4. findOne(data, metadata) {
  5. const items = [
  6. { id: 1, name: 'John' },
  7. { id: 2, name: 'Doe' },
  8. ];
  9. return items.find(({ id }) => id === data.id);
  10. }
  11. }

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., 'HeroesService'), corresponding to the HeroesService service definition in hero.proto. The second (the string 'FindOne') corresponds to the FindOne() rpc method defined within HeroesService 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.

heroes.controller.ts

  1. @Controller()
  2. export class HeroesController {
  3. @GrpcMethod('HeroesService')
  4. findOne(data: HeroById, metadata: any): Hero {
  5. const items = [
  6. { id: 1, name: 'John' },
  7. { id: 2, name: 'Doe' },
  8. ];
  9. return items.find(({ id }) => id === data.id);
  10. }
  11. }
  1. @Controller()
  2. export class HeroesController {
  3. @GrpcMethod('HeroesService')
  4. findOne(data, metadata) {
  5. const items = [
  6. { id: 1, name: 'John' },
  7. { id: 2, name: 'Doe' },
  8. ];
  9. return items.find(({ id }) => id === data.id);
  10. }
  11. }

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 HeroesService associates its handler methods with the HeroesService service definition in the hero.proto file based on the matching of the name 'HeroesService'.

heroes.controller.ts

  1. @Controller()
  2. export class HeroesService {
  3. @GrpcMethod()
  4. findOne(data: HeroById, metadata: any): Hero {
  5. const items = [
  6. { id: 1, name: 'John' },
  7. { id: 2, name: 'Doe' },
  8. ];
  9. return items.find(({ id }) => id === data.id);
  10. }
  11. }
  1. @Controller()
  2. export class HeroesService {
  3. @GrpcMethod()
  4. findOne(data, metadata) {
  5. const items = [
  6. { id: 1, name: 'John' },
  7. { id: 2, name: 'Doe' },
  8. ];
  9. return items.find(({ id }) => id === data.id);
  10. }
  11. }

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.

  1. imports: [
  2. ClientsModule.register([
  3. {
  4. name: 'HERO_PACKAGE',
  5. transport: Transport.GRPC,
  6. options: {
  7. package: 'hero',
  8. protoPath: join(__dirname, 'hero/hero.proto'),
  9. },
  10. },
  11. ]),
  12. ];

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.

  1. @Injectable()
  2. export class AppService implements OnModuleInit {
  3. private heroesService: HeroesService;
  4. constructor(@Inject('HERO_PACKAGE') private client: ClientGrpc) {}
  5. onModuleInit() {
  6. this.heroesService = this.client.getService<HeroesService>('HeroesService');
  7. }
  8. getHero(): Observable<string> {
  9. return this.heroesService.findOne({ id: 1 });
  10. }
  11. }

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:

  1. @Injectable()
  2. export class AppService implements OnModuleInit {
  3. @Client({
  4. transport: Transport.GRPC,
  5. options: {
  6. package: 'hero',
  7. protoPath: join(__dirname, 'hero/hero.proto'),
  8. },
  9. })
  10. client: ClientGrpc;
  11. private heroesService: HeroesService;
  12. onModuleInit() {
  13. this.heroesService = this.client.getService<HeroesService>('HeroesService');
  14. }
  15. getHero(): Observable<string> {
  16. return this.heroesService.findOne({ id: 1 });
  17. }
  18. }

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 HeroesService 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., heroesService), 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, heroesService 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 HeroesService definition contains the FindOne() function, the heroesService instance will provide the findOne() method.

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

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

heroes.controller.ts

  1. @Get()
  2. call(): Observable<any> {
  3. return this.heroesService.findOne({ id: 1 });
  4. }
  1. @Get()
  2. call() {
  3. return this.heroesService.findOne({ id: 1 });
  4. }

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.

Official enterprise support

  • gRPC - 图1 Providing technical guidance
  • gRPC - 图2 Performing in-depth code reviews
  • gRPC - 图3 Mentoring team members
  • gRPC - 图4 Advising best practices

Explore more

Streaming sample

Let’s define a new sample gRPC service called HelloService. The hello.proto file is structured using protocol buffers. Here’s what it looks like:

  1. // hello/hello.proto
  2. syntax = "proto3";
  3. package hello;
  4. service HelloService {
  5. rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
  6. rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
  7. }
  8. message HelloRequest {
  9. string greeting = 1;
  10. }
  11. message HelloResponse {
  12. string reply = 1;
  13. }

Hint The LotsOfGreetings method can be simply implemented with the @GrpcMethod decorator (as in the examples above) since the returned stream can emit multiple values.

Based on this .proto file, let’s define the HelloService interface:

  1. interface HelloService {
  2. bidiHello(upstream: Observable<HelloRequest>): Observable<HelloResponse>;
  3. lotsOfGreetings(
  4. upstream: Observable<HelloRequest>,
  5. ): Observable<HelloResponse>;
  6. }
  7. interface HelloRequest {
  8. greeting: string;
  9. }
  10. interface HelloResponse {
  11. reply: string;
  12. }

Subject strategy

The @GrpcStreamMethod() decorator provides the function parameter as an RxJS Observable. Thus, we can receive and process multiple messages.

  1. @GrpcStreamMethod()
  2. bidiHello(messages: Observable<any>): Observable<any> {
  3. const subject = new Subject();
  4. const onNext = message => {
  5. console.log(message);
  6. subject.next({
  7. reply: 'Hello, world!'
  8. });
  9. };
  10. const onComplete = () => subject.complete();
  11. messages.subscribe(onNext, null, onComplete);
  12. return subject.asObservable();
  13. }

Hint For supporting full-duplex interaction with the @GrpcStreamMethod() decorator, the controller method must return an RxJS Observable.

According to the service definition (in the .proto file), the BidiHello method should stream requests to the service. To send multiple asynchronous messages to the stream from a client, we leverage an RxJS ReplySubject class.

  1. const helloService = this.client.getService<HelloService>('HelloService');
  2. const helloRequest$ = new ReplaySubject<HelloRequest>();
  3. helloRequest$.next({ greeting: 'Hello (1)!' });
  4. helloRequest$.next({ greeting: 'Hello (2)!' });
  5. helloRequest$.complete();
  6. return helloService.bidiHello(helloRequest$);

In the example above, we wrote two messages to the stream (next() calls) and notified the service that we’ve completed sending the data (complete() call).

Call stream handler

When the method return value is defined as stream, 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.

Alternatively, when the method return value is not a stream, the @GrpcStreamCall() decorator provides two function parameters, respectively grpc.ServerReadableStream (read more here) and callback.

Let’s start with implementing the BidiHello which should support a full-duplex interaction.

  1. @GrpcStreamCall()
  2. bidiHello(requestStream: any) {
  3. requestStream.on('data', message => {
  4. console.log(message);
  5. requestStream.write({
  6. reply: 'Hello, world!'
  7. });
  8. });
  9. }

Hint 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.

In the example above, we used the write() method to write objects to the response stream. The callback passed into the .on() method as a second parameter will be called every time our service receives a new chunk of data.

Let’s implement the LotsOfGreetings method.

  1. @GrpcStreamCall()
  2. lotsOfGreetings(requestStream: any, callback: (err: unknown, value: HelloResponse) => void) {
  3. requestStream.on('data', message => {
  4. console.log(message);
  5. });
  6. requestStream.on('end', () => callback(null, { reply: 'Hello, world!' }));
  7. }

Here we used the callback function to send the response once processing of the requestStream has been completed.