gRPC Transport
gRPC is a high-performance, contract-first RPC framework that uses Protocol Buffers over HTTP/2. Unlike the broker- and socket-based transporters, gRPC is defined by a .proto schema that both sides share, giving you strongly typed services, compact binary payloads, and first-class bidirectional streaming. NestJS wraps @grpc/grpc-js so you can expose and consume gRPC services with the same decorator-driven model you use everywhere else.
How the gRPC transporter works
A gRPC service starts from a .proto file. It declares one or more service blocks, each containing rpc methods with request and response message types. NestJS loads this definition at runtime, maps each rpc to a controller method, and handles serialization for you. There is no @MessagePattern here—gRPC methods are matched by service and method name, so NestJS provides dedicated @GrpcMethod and @GrpcStreamMethod decorators.
Because the contract lives in the .proto, clients and servers stay in lockstep: rename a field and both sides see the change. gRPC supports four call types—unary, server streaming, client streaming, and bidirectional streaming—all of which NestJS exposes through RxJS observables on the client.
Installing dependencies
npm install @nestjs/microservices @grpc/grpc-js @grpc/proto-loader
Defining the proto contract
Create a .proto file that describes the service. This single file is the source of truth for both server and client.
// hero.proto
syntax = "proto3";
package hero;
service HeroesService {
rpc FindOne (HeroById) returns (Hero) {}
rpc FindMany (stream HeroById) returns (stream Hero) {}
}
message HeroById {
int32 id = 1;
}
message Hero {
int32 id = 1;
string name = 2;
}
Configuring the server
Bootstrap the microservice with Transport.GRPC. The package must match the proto’s package declaration, and protoPath points at the file on disk.
// main.ts
import { join } from 'path';
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero.proto'),
url: '0.0.0.0:5000',
},
},
);
await app.listen();
}
bootstrap();
Implement the service in a controller. @GrpcMethod(serviceName, methodName) binds a handler to an rpc. If you omit the arguments, NestJS infers the service from the controller class name and the method from the handler name (capitalized).
// heroes.controller.ts
import { Controller } from '@nestjs/common';
import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices';
import { Observable, Subject } from 'rxjs';
interface HeroById {
id: number;
}
interface Hero {
id: number;
name: string;
}
const heroes: Hero[] = [
{ id: 1, name: 'Doombot' },
{ id: 2, name: 'Boombox' },
];
@Controller()
export class HeroesController {
@GrpcMethod('HeroesService', 'FindOne')
findOne(data: HeroById): Hero {
return heroes.find((hero) => hero.id === data.id) ?? { id: 0, name: 'Unknown' };
}
@GrpcStreamMethod('HeroesService', 'FindMany')
findMany(messages: Observable<HeroById>): Observable<Hero> {
const subject = new Subject<Hero>();
messages.subscribe({
next: (request) => {
const hero = heroes.find((h) => h.id === request.id);
if (hero) subject.next(hero);
},
complete: () => subject.complete(),
});
return subject.asObservable();
}
}
Tip: Use
@GrpcMethodfor unary and server-streaming calls, and@GrpcStreamMethodwhen the request is a stream (client or bidirectional). With@GrpcStreamMethodyour handler receives anObservableof incoming messages.
Configuring the client
Register a gRPC client with ClientsModule, pointing at the same proto and package.
// app.module.ts
import { join } from 'path';
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { GatewayController } from './gateway.controller';
@Module({
imports: [
ClientsModule.register([
{
name: 'HERO_PACKAGE',
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero.proto'),
url: 'localhost:5000',
},
},
]),
],
controllers: [GatewayController],
})
export class AppModule {}
Unlike other transporters, a gRPC ClientGrpc is not used directly. In onModuleInit you call getService<T>() to obtain a typed proxy whose methods return observables.
// gateway.controller.ts
import { Controller, Get, Param, Inject, OnModuleInit } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { Observable } from 'rxjs';
interface HeroesService {
findOne(data: { id: number }): Observable<{ id: number; name: string }>;
}
@Controller('heroes')
export class GatewayController implements OnModuleInit {
private heroesService: HeroesService;
constructor(@Inject('HERO_PACKAGE') private readonly client: ClientGrpc) {}
onModuleInit() {
this.heroesService = this.client.getService<HeroesService>('HeroesService');
}
@Get(':id')
findOne(@Param('id') id: string): Observable<{ id: number; name: string }> {
return this.heroesService.findOne({ id: Number(id) });
}
}
Output:
$ curl http://localhost:3000/heroes/1
{"id":1,"name":"Doombot"}
Transporter options
These live under the options key and configure both the loader and the channel.
| Option | Type | Purpose |
|---|---|---|
package | string | Proto package name (or array for multiple). |
protoPath | string | Absolute path to the .proto file. |
url | string | Host and port to bind/connect (default localhost:5000). |
loader | object | proto-loader options (keepCase, longs, enums, defaults). |
credentials | ChannelCredentials | TLS credentials for secure channels. |
maxReceiveMessageLength | number | Max inbound message size in bytes. |
maxSendMessageLength | number | Max outbound message size in bytes. |
Warning: By default
proto-loaderconverts proto field names to camelCase andint64values tostring. Setloader: { keepCase: true, longs: Number }if your TypeScript interfaces expect snake_case keys or numeric longs—mismatches surface as silentlyundefinedfields.
Streaming RPCs
Server streaming returns multiple responses to a single request; the client subscribes and receives a sequence of emissions. Client streaming sends many requests and gets one response. Bidirectional streaming runs both at once. On the NestJS client, every shape is just an Observable, so you compose them with standard RxJS operators (map, toArray, take) rather than callbacks.
Best practices
- Keep the
.protofiles in a shared package or repo so server and client never drift out of sync. - Always set explicit
loaderoptions (keepCase,longs,enums) so wire types match your TypeScript interfaces predictably. - Define a typed interface for each gRPC service and pass it to
getService<T>()for end-to-end type safety. - Use
@GrpcStreamMethodonly when the request is a stream; reserve@GrpcMethodfor unary and server-streaming responses. - Enable TLS via
credentialsin production and bind plaintext only inside trusted networks. - Set
maxReceiveMessageLength/maxSendMessageLengthdeliberately for large payloads instead of relying on the 4 MB default. - Version your package or service names (e.g.
hero.v1) so you can evolve the contract without breaking existing clients.