Skip to content
NestJS ns microservices 5 min read

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 @GrpcMethod for unary and server-streaming calls, and @GrpcStreamMethod when the request is a stream (client or bidirectional). With @GrpcStreamMethod your handler receives an Observable of 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.

OptionTypePurpose
packagestringProto package name (or array for multiple).
protoPathstringAbsolute path to the .proto file.
urlstringHost and port to bind/connect (default localhost:5000).
loaderobjectproto-loader options (keepCase, longs, enums, defaults).
credentialsChannelCredentialsTLS credentials for secure channels.
maxReceiveMessageLengthnumberMax inbound message size in bytes.
maxSendMessageLengthnumberMax outbound message size in bytes.

Warning: By default proto-loader converts proto field names to camelCase and int64 values to string. Set loader: { keepCase: true, longs: Number } if your TypeScript interfaces expect snake_case keys or numeric longs—mismatches surface as silently undefined fields.

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 .proto files in a shared package or repo so server and client never drift out of sync.
  • Always set explicit loader options (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 @GrpcStreamMethod only when the request is a stream; reserve @GrpcMethod for unary and server-streaming responses.
  • Enable TLS via credentials in production and bind plaintext only inside trusted networks.
  • Set maxReceiveMessageLength/maxSendMessageLength deliberately 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.
Last updated June 14, 2026
Was this helpful?