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
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero/hero.proto'),
},
});
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 thepath
package; theTransport
enum is imported from the@nestjs/microservices
package.
Options
The gRPC transporter options object exposes the properties described below.
package | Protobuf package name (matches package setting from .proto file). Required |
protoPath | Absolute (or relative to the root dir) path to the .proto file. Required |
url | Connection 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’ |
protoLoader | NPM 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 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:
// hero/hero.proto
syntax = "proto3";
package hero;
service HeroesService {
rpc FindOne (HeroById) returns (Hero) {}
}
message HeroById {
int32 id = 1;
}
message Hero {
int32 id = 1;
string name = 2;
}
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
@Controller()
export class HeroesController {
@GrpcMethod('HeroesService', '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 HeroesController {
@GrpcMethod('HeroesService', '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., '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
@Controller()
export class HeroesController {
@GrpcMethod('HeroesService')
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 HeroesController {
@GrpcMethod('HeroesService')
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 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
@Controller()
export class HeroesService {
@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 HeroesService {
@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 heroesService: HeroesService;
constructor(@Inject('HERO_PACKAGE') private client: ClientGrpc) {}
onModuleInit() {
this.heroesService = this.client.getService<HeroesService>('HeroesService');
}
getHero(): Observable<string> {
return this.heroesService.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 heroesService: HeroesService;
onModuleInit() {
this.heroesService = this.client.getService<HeroesService>('HeroesService');
}
getHero(): Observable<string> {
return this.heroesService.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 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.
interface HeroesService {
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.
heroes.controller.ts
@Get()
call(): Observable<any> {
return this.heroesService.findOne({ id: 1 });
}
@Get()
call() {
return this.heroesService.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 toSubject
/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
- Providing technical guidance
- Performing in-depth code reviews
- Mentoring team members
- Advising best practices
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:
// hello/hello.proto
syntax = "proto3";
package hello;
service HelloService {
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string greeting = 1;
}
message HelloResponse {
string reply = 1;
}
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:
interface HelloService {
bidiHello(upstream: Observable<HelloRequest>): Observable<HelloResponse>;
lotsOfGreetings(
upstream: Observable<HelloRequest>,
): Observable<HelloResponse>;
}
interface HelloRequest {
greeting: string;
}
interface HelloResponse {
reply: string;
}
Subject strategy
The @GrpcStreamMethod()
decorator provides the function parameter as an RxJS Observable
. Thus, we can receive and process multiple messages.
@GrpcStreamMethod()
bidiHello(messages: Observable<any>): Observable<any> {
const subject = new Subject();
const onNext = message => {
console.log(message);
subject.next({
reply: 'Hello, world!'
});
};
const onComplete = () => subject.complete();
messages.subscribe(onNext, null, onComplete);
return subject.asObservable();
}
Hint For supporting full-duplex interaction with the
@GrpcStreamMethod()
decorator, the controller method must return an RxJSObservable
.
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.
const helloService = this.client.getService<HelloService>('HelloService');
const helloRequest$ = new ReplaySubject<HelloRequest>();
helloRequest$.next({ greeting: 'Hello (1)!' });
helloRequest$.next({ greeting: 'Hello (2)!' });
helloRequest$.complete();
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.
@GrpcStreamCall()
bidiHello(requestStream: any) {
requestStream.on('data', message => {
console.log(message);
requestStream.write({
reply: 'Hello, world!'
});
});
}
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.
@GrpcStreamCall()
lotsOfGreetings(requestStream: any, callback: (err: unknown, value: HelloResponse) => void) {
requestStream.on('data', message => {
console.log(message);
});
requestStream.on('end', () => callback(null, { reply: 'Hello, world!' }));
}
Here we used the callback
function to send the response once processing of the requestStream
has been completed.