gRPC
gRPC
The gRPC is a high-performance, open-source universal RPC framework.
Installation
Before we start, we have to install required package:
$ 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.
@@filename(main)
const app = await NestFactory.createMicroservice(ApplicationModule, {
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero/hero.proto'),
},
});
info Hint The
join()function is imported frompathpackage, whileTransportenum 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.
syntax = "proto3";
package hero;
service HeroService {
rpc FindOne (HeroById) returns (Hero) {}
}
message HeroById {
int32 id = 1;
}
message Hero {
int32 id = 1;
string name = 2;
}
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.
@@filename(hero.controller)
@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);
}
@@switch
@GrpcMethod('HeroService', 'FindOne')
findOne(data, metadata) {
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];
return items.find(({ id }) => id === data.id);
}
info Hint The
@GrpcMethod()decorator is imported from@nestjs/microservicespackage.
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.
@@filename(hero.controller)
@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);
}
}
@@switch
@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);
}
}
Likewise, you might not pass any argument. In this case, Nest would use a class name.
@@filename(hero.controller)
@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);
}
}
@@switch
@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
In order to create a client instance, we need to use @Client() decorator.
@Client({
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero/hero.proto'),
},
})
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.
@@filename(hero.controller)
onModuleInit() {
this.heroService = this.client.getService<HeroService>('HeroService');
}
@@switch
onModuleInit() {
this.heroService = this.client.getService('HeroService');
}
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.
interface HeroService {
findOne(data: { id: number }): Observable<any>;
}
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.
@@filename(hero.controller)
@Get()
call(): Observable<any> {
return this.heroService.findOne({ id: 1 });
}
@@switch
@Get()
call() {
return this.heroService.findOne({ id: 1 });
}
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+Observablehandler: can be useful to writeresponses right inside of a Controller method or to be passed downtoSubject/Observableconsumer - Pure GRPC call stream handler: can be useful to be passedto some executor which will handle the rest of dispatch forthe Node standard
Duplexstream handler.
Subject strategy
@GrpcStreamMethod() decorator will provide the function parameter as 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 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.
// 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 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.
