gRPC
gRPC is a modern, open source, high-performance RPC framework that can run in any environment. It connects 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 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, which makes remote procedure calls straightforward to implement and serializes and deserializes structured data automatically.
Installation#
To start building gRPC-based microservices, first install the required packages:
$ npm i --save @grpc/grpc-js @grpc/proto-loader
Overview#
As with other Nest microservice transport layer implementations, you select the gRPC transporter 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 in Options below.
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(import.meta.dirname, 'hero/hero.proto'),
},
});
const app = await NestFactory.createMicroservice(AppModule, {
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(import.meta.dirname, 'hero/hero.proto'),
},
});
Hint Thejoin()function is imported from thepathpackage, and theTransportenum from the@nestjs/microservicespackage.
In the nest-cli.json file, add the assets property to distribute non-TypeScript files, and watchAssets to watch all non-TypeScript assets. In this case, we want .proto files to be copied to the dist folder automatically.
{
"compilerOptions": {
"assets": ["**/*.proto"],
"watchAssets": true
}
}
Options#
The gRPC transporter options object exposes the following properties.
package | Protobuf package name (matches the package setting in the .proto file). Also accepts an array of package names. Required |
protoPath | Absolute (or relative to the current working directory) path to the .proto file, or an array of paths. Required unless you pass a preloaded packageDefinition instead |
url | Connection URL. A string in the format ip address/dns name:port (e.g., '0.0.0.0:50051' for a Docker server) that defines the address and 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, which control how .proto files are loaded. Optional. See the proto-loader README for details |
credentials | Server credentials (channel credentials when used by a client). Optional. Defaults to insecure credentials. See the ServerCredentials reference |
Sample gRPC service#
Let's define a sample gRPC service called HeroesService. In the options object above, the protoPath property sets the path to the hero.proto definitions file. The hero.proto file is structured using protocol buffers:
// 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;
}
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, implement the service. To define a handler that fulfills this definition, 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 (see request-response) introduced in previous microservices chapters is not used with gRPC-based microservices. The@GrpcMethod()decorator takes its place.
@Controller()
export class HeroesController {
@GrpcMethod('HeroesService', 'FindOne')
findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<any, 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, call) {
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/microservicespackage, whileMetadataandServerUnaryCallare imported from the@grpc/grpc-jspackage.
The decorator 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 hero.proto.
The findOne() handler method takes three arguments: the data passed from the caller, the metadata that stores gRPC request metadata, and the call object, which exposes members such as sendMetadata() for sending metadata to the client.
Both @GrpcMethod() decorator arguments are optional. If you omit the second argument (e.g., 'FindOne'), Nest associates the handler with the .proto rpc method whose name matches the handler name with its first letter capitalized (e.g., the findOne handler is associated with the FindOne rpc definition), as shown below.
@Controller()
export class HeroesController {
@GrpcMethod('HeroesService')
findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<any, 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, call) {
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 associates the handler with the service definition in the .proto file based on the name of the class where the handler is defined. For example, in the following code, the HeroesService class associates its handler methods with the HeroesService service definition in hero.proto because the names match.
@Controller()
export class HeroesService {
@GrpcMethod()
findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<any, 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, call) {
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, which you can obtain in several ways.
The preferred technique is to import the ClientsModule. Use its 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 in Options above.
imports: [
ClientsModule.register([
{
name: 'HERO_PACKAGE',
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(import.meta.dirname, 'hero/hero.proto'),
},
},
]),
];
Hint The register() method takes an array of objects. To register multiple packages, provide a comma-separated list of registration objects.
Once registered, inject the configured ClientGrpc object with @Inject(). Then 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 });
}
}
Warning The gRPC client does not send fields that contain an underscore (_) in their names unless thekeepCaseoption is set totruein the proto loader configuration (options.loader.keepCasein the microservice transporter configuration).
This differs slightly from the technique used with other microservice transporters. Instead of the ClientProxy class, you use the ClientGrpc class, which provides the getService() method. The generic getService() 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(import.meta.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, you can inject a dynamically configured client using the ClientProxyFactory class, as described in the client section of the microservices overview.
In each case, you end up with a reference to the HeroesService proxy object, which exposes the same set of methods that are defined in the .proto file. When you call this proxy object (i.e., heroesService), gRPC serializes the request, forwards it to the remote system, returns a response, and deserializes the response. Because gRPC shields you from these network communication details, heroesService looks and acts like a local provider.
All service methods are lower camel cased, following the natural convention of the language. For example, while the HeroesService definition in the .proto file contains the FindOne() function, the heroesService instance provides the findOne() method.
interface HeroesService {
findOne(data: { id: number }): Observable<any>;
}
A handler can also return an Observable, in which case the result values are emitted until the stream completes.
@Get()
call(): Observable<any> {
return this.heroesService.findOne({ id: 1 });
}
@Get()
call() {
return this.heroesService.findOne({ id: 1 });
}
To send gRPC metadata along with the request, pass a second argument, as follows:
call(): Observable<any> {
const metadata = new Metadata();
metadata.add('Set-Cookie', 'yummy_cookie=choco');
return this.heroesService.findOne({ id: 1 }, metadata);
}
Hint TheMetadataclass is imported from the@grpc/grpc-jspackage.
This requires updating the HeroesService interface defined earlier to accept the second argument.
Exception handling#
gRPC handlers can throw RpcException, but a plain RpcException does not carry a gRPC status code, so clients receive UNKNOWN for every failure. Starting with NestJS v12, the @nestjs/microservices package ships dedicated gRPC exceptions and a GrpcExceptionFilter that maps them to proper gRPC error objects.
Throw one of the status-specific exceptions from your handler:
import { GrpcAlreadyExistsException } from '@nestjs/microservices';
@GrpcMethod('HeroesService')
create(data: Hero): Hero {
if (this.heroes.has(data.id)) {
throw new GrpcAlreadyExistsException('Hero already exists');
}
return this.heroes.add(data);
}
Then register the filter so those exceptions are serialized into gRPC errors:
import { GrpcExceptionFilter } from '@nestjs/microservices';
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(import.meta.dirname, 'hero/hero.proto'),
},
});
app.useGlobalFilters(new GrpcExceptionFilter());
With the filter in place, the client receives ALREADY_EXISTS instead of UNKNOWN.
You can also use the generic GrpcException and pass a status explicitly:
import { GrpcException, GrpcStatus } from '@nestjs/microservices';
throw new GrpcException('Rate limit exceeded', GrpcStatus.RESOURCE_EXHAUSTED);
The following status-specific exception classes are available. Each corresponds to a member of the GrpcStatus enum:
GrpcCancelledException | GrpcUnknownException | GrpcInvalidArgumentException | GrpcDeadlineExceededException |
GrpcNotFoundException | GrpcAlreadyExistsException | GrpcPermissionDeniedException | GrpcResourceExhaustedException |
GrpcFailedPreconditionException | GrpcAbortedException | GrpcOutOfRangeException | GrpcUnimplementedException |
GrpcInternalException | GrpcUnavailableException | GrpcDataLossException | GrpcUnauthenticatedException |
HintGrpcExceptionFilteralso handlesRpcException. If the error object passed to theRpcExceptionconstructor carries a numericcodeorstatusproperty, that value is used as the gRPC status; otherwise, the error is reported asUNKNOWN. Any other exception is reported asUNKNOWNwith a generic message.
Example#
A working example is available in the gRPC sample of the NestJS repository.
gRPC Reflection#
The gRPC Server Reflection Specification is a standard that allows gRPC clients to request details about the API the server exposes, similar to exposing an OpenAPI document for a REST API. This makes it easier to work with debugging tools such as grpc-ui or Postman.
To add gRPC reflection support to your server, first install the implementation package:
$ npm i --save @grpc/reflection
Then hook it into the gRPC server using the onLoadPackageDefinition hook in your gRPC server options, as follows:
import { ReflectionService } from '@grpc/reflection';
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(import.meta.dirname, 'hero/hero.proto'),
onLoadPackageDefinition: (pkg, server) => {
new ReflectionService(pkg).addToServer(server);
},
},
});
Your server now responds to reflection requests for API details.
gRPC Streaming#
gRPC supports long-lived connections, conventionally known as streams. Streams are useful for cases such as chat, observation, or chunked data transfers. See the gRPC core concepts guide for details.
Nest supports gRPC stream handlers in two ways:
- RxJS
Subject+Observablehandler: useful for writing responses directly inside a controller method, or for passing them to aSubject/Observableconsumer - Pure gRPC call stream handler: useful for passing the call to an executor that handles the rest of the dispatch for the standard Node.js
Duplexstream
Streaming sample#
Let's define a new sample gRPC service called HelloService. The hello.proto file is structured using protocol buffers:
// 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;
}
HintLotsOfGreetingsstreams requests but returns a single response. Because its requests are streamed, it can't be implemented with@GrpcMethod(); use@GrpcStreamMethod()(the last value the returnedObservableemits is sent as the response) or@GrpcStreamCall()(respond through the callback), both shown below.
Based on this .proto file, 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;
}
Hint The proto interface can be generated automatically by the ts-proto package. See the ts-proto NestJS guide to learn more.
Subject strategy#
The @GrpcStreamMethod() decorator provides the function parameter as an RxJS Observable, so the handler can receive and process multiple messages.
@GrpcStreamMethod()
bidiHello(messages: Observable<any>, metadata: Metadata, call: ServerDuplexStream<any, any>): Observable<any> {
const subject = new Subject();
const onNext = message => {
console.log(message);
subject.next({
reply: 'Hello, world!'
});
};
const onComplete = () => subject.complete();
messages.subscribe({
next: onNext,
complete: onComplete,
});
return subject.asObservable();
}
Warning To support full-duplex interaction with the@GrpcStreamMethod()decorator, the controller method must return an RxJSObservable.
Hint TheMetadataclass and theServerDuplexStreaminterface are imported from the@grpc/grpc-jspackage.
According to the service definition in the .proto file, the BidiHello method streams requests to the service. To send multiple asynchronous messages to the stream from a client, use an RxJS ReplaySubject.
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 (the next() calls) and notified the service that we finished sending data (the complete() call).
Call stream handler#
When the method's response is defined as a stream, the @GrpcStreamCall() decorator provides the function parameter as a grpc.ServerDuplexStream, which supports standard methods such as .on('data', callback), .write(message), and .end(). See the gRPC Node.js API reference for the full list of methods.
When the method's response is not a stream, the @GrpcStreamCall() decorator provides two function parameters: a grpc.ServerReadableStream (see the ServerReadableStream reference) and a callback.
Let's start by implementing BidiHello, which supports full-duplex interaction.
@GrpcStreamCall()
bidiHello(requestStream: any) {
requestStream.on('data', message => {
console.log(message);
requestStream.write({
reply: 'Hello, world!'
});
});
}
Hint This decorator doesn't require the handler to return anything. You handle the stream as you would any other standard stream.
In the example above, we used the write() method to write objects to the response stream. The callback passed to the .on() method as the second parameter is called every time the service receives a new chunk of data.
Next, 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, the callback function sends the response once the requestStream has been fully processed.
Health checks#
When you run a gRPC application in an orchestrator such as Kubernetes, the orchestrator needs to know whether the application is running and healthy. The gRPC Health Check specification is a standard that lets gRPC servers expose their health status so that the orchestrator can act accordingly.
To add gRPC health check support, first install the grpc-health-check package:
$ npm i --save grpc-health-check
Then hook it into the gRPC service using the onLoadPackageDefinition hook in your gRPC server options, as follows. The protoPath must include both the health check and the hero .proto files.
import { HealthImplementation, protoPath as healthCheckProtoPath } from 'grpc-health-check';
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: [
healthCheckProtoPath,
join(import.meta.dirname, 'hero/hero.proto'),
],
onLoadPackageDefinition: (pkg, server) => {
const healthImpl = new HealthImplementation({
'': 'UNKNOWN',
});
healthImpl.addToServer(server);
healthImpl.setStatus('', 'SERVING');
},
},
});
Hint The gRPC health probe is a CLI for testing gRPC health checks in a containerized environment.
gRPC Metadata#
Metadata is information about a particular RPC call in the form of a list of key-value pairs, where the keys are strings and the values are typically strings but can be binary data. Metadata is opaque to gRPC itself: it lets the client provide information associated with the call to the server, and vice versa. Metadata may include authentication tokens, request identifiers and tags for monitoring purposes, and data information such as the number of records in a data set.
To read the metadata in a @GrpcMethod() handler, use the second argument (metadata), which is of type Metadata (imported from the @grpc/grpc-js package).
To send metadata back from the handler, use the ServerUnaryCall#sendMetadata() method (on the third handler argument).
@Controller()
export class HeroesService {
@GrpcMethod()
findOne(data: HeroById, metadata: Metadata, call: ServerUnaryCall<any, any>): Hero {
const serverMetadata = new Metadata();
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];
serverMetadata.add('Set-Cookie', 'yummy_cookie=choco');
call.sendMetadata(serverMetadata);
return items.find(({ id }) => id === data.id);
}
}
@Controller()
export class HeroesService {
@GrpcMethod()
findOne(data, metadata, call) {
const serverMetadata = new Metadata();
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];
serverMetadata.add('Set-Cookie', 'yummy_cookie=choco');
call.sendMetadata(serverMetadata);
return items.find(({ id }) => id === data.id);
}
}
Likewise, to read the metadata in handlers annotated with the @GrpcStreamMethod() decorator (see Subject strategy), use the second argument (metadata), which is of type Metadata (imported from the @grpc/grpc-js package).
To send metadata back from the handler, use the ServerDuplexStream#sendMetadata() method (on the third handler argument).
To read metadata from within call stream handlers (handlers annotated with the @GrpcStreamCall() decorator), listen to the metadata event on the requestStream reference, as follows:
requestStream.on('metadata', (metadata: Metadata) => {
const meta = metadata.get('X-Meta');
});

