Protocolos & APIs

gRPC

Referência completa de gRPC — Protocol Buffers, tipos de comunicação, streaming, interceptors, autenticação, Node.js e comparativo com REST

Fundamentos gRPC

gRPC é um framework de Remote Procedure Call (RPC) open-source desenvolvido pelo Google, construído sobre HTTP/2 e usando Protocol Buffers como Interface Definition Language (IDL) padrão.

gRPC vs REST vs GraphQL

CritériogRPCRESTGraphQL
ProtocoloHTTP/2HTTP/1.1 ou 2HTTP/1.1 ou 2
FormatoProtocol Buffers (binário)JSON/XMLJSON
Contrato.proto (obrigatório)OpenAPI (opcional)Schema GraphQL
StreamingNativo (4 tipos)SSE / WebSocketSubscriptions
BrowserLimitado (grpc-web)TotalTotal
Code GenNativoOpcionalOpcional

Protocol Buffers como IDL

O .proto é o contrato central: define mensagens, campos e serviços. O protoc gera código client/server em qualquer linguagem suportada, garantindo compatibilidade binária.

HTTP/2 como Transport

  • Multiplexing de chamadas na mesma conexão TCP
  • Header compression (HPACK)
  • Server push (base do streaming)
  • Conexão persistente com keepalive

Vantagens

  • Performance: serialização binária 3–10x menor que JSON; sem parsing de texto
  • Tipagem forte: contrato versionável, breaking changes detectáveis em CI
  • Streaming nativo: 4 tipos de comunicação, incluindo bidirecional
  • Code generation: cliente e servidor gerados a partir do .proto
  • Interoperabilidade: Go, Java, Node.js, Python, Rust etc. com o mesmo contrato

Desvantagens

  • Suporte limitado em browsers (requer grpc-web + proxy)
  • Debugging mais difícil: payload binário não é legível diretamente no Wireshark/DevTools
  • Curva de aprendizado do ecossistema protoc + plugins
  • Sem cache HTTP nativo (cada chamada é POST)

Quando Usar

  • Comunicação interna entre microservices (backend-to-backend)
  • Workloads com streaming contínuo (telemetria, live updates, uploads)
  • Mobile/IoT com restrição de banda (payload menor)
  • Quando tipagem forte e geração de código são prioritários

Protocol Buffers (proto3)

Estrutura de um Arquivo .proto

syntax = "proto3";

package orders.v1;

option go_package = "github.com/example/orders/v1;ordersv1";
option java_package = "com.example.orders.v1";
option java_outer_classname = "OrdersProto";

import "google/protobuf/timestamp.proto";
import "google/protobuf/any.proto";

message Order {
  string  id          = 1;
  string  customer_id = 2;
  double  total       = 3;
  Status  status      = 4;
  repeated OrderItem items = 5;
  google.protobuf.Timestamp created_at = 6;
}

Scalar Types

Proto3JavaScriptNotas
doublenumber64-bit float
floatnumber32-bit float
int32numberVarlength, negativo ineficiente
int64string/LongJS não suporta int64 nativo
uint32numberSem sinal
uint64string/LongSem sinal
sint32numberZigZag, melhor para negativos
boolboolean
stringstringUTF-8
bytesBufferDados binários

Field Numbers e Reserved

message User {
  // Field numbers 1–15 usam 1 byte (prefira para campos frequentes)
  string id    = 1;
  string email = 2;
  string name  = 3;

  // Nunca reutilize field numbers removidos
  reserved 4, 5;
  reserved "phone", "address";
}

Enums

enum Status {
  STATUS_UNSPECIFIED = 0; // obrigatório em proto3
  STATUS_PENDING     = 1;
  STATUS_CONFIRMED   = 2;
  STATUS_SHIPPED     = 3;
  STATUS_DELIVERED   = 4;
  STATUS_CANCELLED   = 5;
}

oneof

message PaymentMethod {
  oneof method {
    CreditCard  credit_card  = 1;
    PixPayment  pix          = 2;
    BankSlip    bank_slip    = 3;
  }
}

Well-Known Types

import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/any.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/wrappers.proto"; // StringValue, Int32Value (nullable)

message Event {
  google.protobuf.Timestamp occurred_at = 1;  // instante no tempo
  google.protobuf.Duration  ttl         = 2;  // duração
  google.protobuf.Any       payload     = 3;  // tipo dinâmico
  google.protobuf.Struct    metadata    = 4;  // JSON-like dinâmico
  google.protobuf.StringValue note      = 5;  // nullable string
}

Tipos de Serviço

Unary RPC

Requisição única → resposta única. Equivalente a uma chamada HTTP tradicional.

service OrderService {
  rpc GetOrder (GetOrderRequest) returns (Order);
}

Quando usar: busca por ID, criação de recurso, operações simples.

Server Streaming

Requisição única → stream de respostas. Servidor envia múltiplas mensagens.

service OrderService {
  rpc ListOrders (ListOrdersRequest) returns (stream Order);
}

Quando usar: paginação de grandes datasets, eventos em tempo real (SSE), exportação de dados.

Client Streaming

Stream de requisições → resposta única. Cliente envia múltiplas mensagens, servidor responde ao final.

service FileService {
  rpc UploadFile (stream FileChunk) returns (UploadResponse);
}

Quando usar: upload de arquivos em chunks, batch insert, agregação de métricas.

Bidirectional Streaming

Stream de requisições ↔ stream de respostas. Totalmente assíncrono.

service ChatService {
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

Quando usar: chat, live collaboration, jogos, telemetria bidirecional.


Implementação Node.js (@grpc/grpc-js)

Instalação

// Dependências principais
npm install @grpc/grpc-js @grpc/proto-loader

// Code generation com ts-proto (TypeScript)
npm install --save-dev protoc ts-proto

// Ou grpc-tools (gerador oficial)
npm install --save-dev grpc-tools

Geração de Código com protoc + ts-proto

# Estrutura de diretórios recomendada
proto/
  orders/v1/orders.proto
  users/v1/users.proto
src/
  generated/   # arquivos gerados

# Gerar com ts-proto
protoc \
  --plugin=./node_modules/.bin/protoc-gen-ts_proto \
  --ts_proto_out=./src/generated \
  --ts_proto_opt=outputServices=grpc-js \
  --ts_proto_opt=esModuleInterop=true \
  --proto_path=./proto \
  ./proto/orders/v1/orders.proto

Criar Servidor

import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import path from 'path';

const PROTO_PATH = path.resolve('./proto/orders/v1/orders.proto');

const packageDef = protoLoader.loadSync(PROTO_PATH, {
  keepCase: false,     // converte snake_case → camelCase
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});

const proto = grpc.loadPackageDefinition(packageDef) as any;

const server = new grpc.Server();

server.addService(proto.orders.v1.OrderService.service, {
  getOrder: getOrderHandler,
  listOrders: listOrdersHandler,
});

server.bindAsync(
  '0.0.0.0:50051',
  grpc.ServerCredentials.createInsecure(), // use createSsl() em produção
  (err, port) => {
    if (err) throw err;
    console.log(`gRPC server listening on port ${port}`);
  }
);

Criar Canal (Channel) e Stub Cliente

import * as grpc from '@grpc/grpc-js';

const channel = new grpc.Channel(
  'orders-service:50051',
  grpc.credentials.createInsecure(),
  {
    // keepalive — evita fechamento de conexão ociosa
    'grpc.keepalive_time_ms': 10000,
    'grpc.keepalive_timeout_ms': 5000,
    'grpc.keepalive_permit_without_calls': 1,
    'grpc.http2.max_pings_without_data': 0,
  }
);

const client = new proto.orders.v1.OrderService(
  'orders-service:50051',
  grpc.credentials.createInsecure(),
  { channelOverride: channel }
);

Deadline / Timeout por Chamada

// Deadline é absoluto (Date), não relativo
const deadline = new Date(Date.now() + 5000); // 5 segundos

client.getOrder(
  { id: 'order-123' },
  { deadline },
  (err, response) => {
    if (err?.code === grpc.status.DEADLINE_EXCEEDED) {
      console.error('Chamada expirou');
    }
  }
);

Unary RPC — Exemplo Completo

Definição .proto

syntax = "proto3";
package orders.v1;

import "google/protobuf/timestamp.proto";

service OrderService {
  rpc GetOrder    (GetOrderRequest)    returns (Order);
  rpc CreateOrder (CreateOrderRequest) returns (Order);
}

message GetOrderRequest {
  string id = 1;
}

message CreateOrderRequest {
  string customer_id = 1;
  repeated OrderItem items = 2;
}

message Order {
  string   id          = 1;
  string   customer_id = 2;
  double   total       = 3;
  Status   status      = 4;
  repeated OrderItem items = 5;
  google.protobuf.Timestamp created_at = 6;
}

message OrderItem {
  string product_id = 1;
  int32  quantity   = 2;
  double price      = 3;
}

enum Status {
  STATUS_UNSPECIFIED = 0;
  STATUS_PENDING     = 1;
  STATUS_CONFIRMED   = 2;
  STATUS_CANCELLED   = 3;
}

Implementação do Servidor (Handlers)

import * as grpc from '@grpc/grpc-js';

async function getOrderHandler(call, callback) {
  const { id } = call.request;

  // Ler metadata (ex: correlation ID)
  const correlationId = call.metadata.get('x-correlation-id')[0];

  try {
    const order = await orderRepository.findById(id);

    if (!order) {
      return callback({
        code: grpc.status.NOT_FOUND,
        message: `Order ${id} not found`,
      });
    }

    // Enviar metadata de resposta (initial metadata antes do response)
    const responseMetadata = new grpc.Metadata();
    responseMetadata.set('x-request-id', correlationId ?? 'unknown');
    call.sendMetadata(responseMetadata);

    callback(null, order);
  } catch (err) {
    callback({
      code: grpc.status.INTERNAL,
      message: 'Internal server error',
    });
  }
}

async function createOrderHandler(call, callback) {
  const { customerId, items } = call.request;

  if (!customerId || items.length === 0) {
    return callback({
      code: grpc.status.INVALID_ARGUMENT,
      message: 'customer_id and at least one item are required',
    });
  }

  try {
    const order = await orderRepository.create({ customerId, items });
    callback(null, order);
  } catch (err) {
    if (err.code === 'DUPLICATE') {
      return callback({ code: grpc.status.ALREADY_EXISTS, message: err.message });
    }
    callback({ code: grpc.status.INTERNAL, message: 'Failed to create order' });
  }
}

Cliente Fazendo Chamada

// Promisify para async/await
import { promisify } from 'util';

const getOrder = promisify(client.getOrder.bind(client));
const createOrder = promisify(client.createOrder.bind(client));

async function fetchOrder(id: string) {
  const metadata = new grpc.Metadata();
  metadata.set('x-correlation-id', crypto.randomUUID());
  metadata.set('authorization', `Bearer ${token}`);

  const deadline = new Date(Date.now() + 3000);

  try {
    const order = await getOrder({ id }, { metadata, deadline });
    console.log('Order:', order);
    return order;
  } catch (err: any) {
    console.error(`gRPC error [${err.code}]: ${err.message}`);
    throw err;
  }
}

Streaming

Server Streaming — Progresso de Longa Operação

// Servidor
function listOrdersHandler(call) {
  const { customerId, pageSize = 50 } = call.request;
  let offset = 0;

  async function sendBatch() {
    const batch = await orderRepo.findByCustomer(customerId, { offset, limit: pageSize });

    for (const order of batch) {
      call.write(order); // envia cada item para o cliente
    }

    if (batch.length === pageSize) {
      offset += pageSize;
      setImmediate(sendBatch); // continua sem bloquear event loop
    } else {
      call.end(); // sinaliza fim do stream
    }
  }

  sendBatch().catch(err => call.destroy(err));
}

// Cliente
function streamOrders(customerId: string) {
  const stream = client.listOrders({ customerId });

  stream.on('data', (order) => {
    console.log('Received:', order.id);
  });

  stream.on('end', () => {
    console.log('Stream completed');
  });

  stream.on('error', (err) => {
    console.error('Stream error:', err);
  });
}

Client Streaming — Upload em Chunks

// Cliente
async function uploadFile(filePath: string) {
  return new Promise((resolve, reject) => {
    const stream = client.uploadFile((err, response) => {
      if (err) reject(err);
      else resolve(response);
    });

    const fileStream = fs.createReadStream(filePath, { highWaterMark: 64 * 1024 }); // 64KB chunks

    fileStream.on('data', (chunk) => {
      stream.write({ data: chunk, filename: path.basename(filePath) });
    });

    fileStream.on('end', () => stream.end());
    fileStream.on('error', (err) => stream.destroy(err));
  });
}

// Servidor
function uploadFileHandler(call, callback) {
  const chunks: Buffer[] = [];
  let filename = '';

  call.on('data', (chunk) => {
    filename = chunk.filename;
    chunks.push(chunk.data);
  });

  call.on('end', async () => {
    const buffer = Buffer.concat(chunks);
    const url = await storageService.save(filename, buffer);
    callback(null, { url, size: buffer.length });
  });
}

Bidirectional Streaming — Chat / Live Updates

// Servidor
function chatHandler(call) {
  call.on('data', (message) => {
    const response = {
      id: crypto.randomUUID(),
      from: 'server',
      text: `Echo: ${message.text}`,
      sentAt: new Date().toISOString(),
    };
    call.write(response);
  });

  call.on('end', () => call.end());
  call.on('error', (err) => console.error('Chat stream error:', err));
}

// Cliente
function startChat() {
  const stream = client.chat();

  stream.on('data', (msg) => console.log(`[${msg.from}]: ${msg.text}`));
  stream.on('end', () => console.log('Chat ended'));

  // Enviar mensagens
  stream.write({ text: 'Hello server!', from: 'client' });

  setTimeout(() => {
    stream.write({ text: 'Goodbye!', from: 'client' });
    stream.end(); // cliente sinaliza fim
  }, 2000);
}

Status Codes gRPC

CódigoNomeHTTP equiv.Quando usar
0OK200Sucesso
1CANCELLED499Cancelado pelo cliente
2UNKNOWN500Erro desconhecido
3INVALID_ARGUMENT400Parâmetro inválido na requisição
4DEADLINE_EXCEEDED504Timeout antes da conclusão
5NOT_FOUND404Recurso não encontrado
6ALREADY_EXISTS409Tentativa de criar recurso duplicado
7PERMISSION_DENIED403Sem permissão
8RESOURCE_EXHAUSTED429Rate limit ou quota esgotada
9FAILED_PRECONDITION400Estado inválido para operação
10ABORTED409Conflito de concorrência (ex: CAS)
11OUT_OF_RANGE400Valor fora do intervalo válido
12UNIMPLEMENTED501Método não implementado
13INTERNAL500Erro interno do servidor
14UNAVAILABLE503Serviço temporariamente indisponível
15DATA_LOSS500Dados corrompidos ou perdidos
16UNAUTHENTICATED401Sem credenciais válidas
// Usar no servidor
callback({
  code: grpc.status.NOT_FOUND,
  message: 'Order not found',
  metadata: new grpc.Metadata(), // detalhes extras
});

// Verificar no cliente
if (err.code === grpc.status.UNAUTHENTICATED) {
  await refreshToken();
  // retry...
}

Metadata (Headers)

Metadata em gRPC equivale a HTTP headers: pares chave-valor enviados junto com a chamada.

Enviar Metadata na Chamada (Cliente)

const metadata = new grpc.Metadata();

// Valores string
metadata.set('authorization', `Bearer ${jwtToken}`);
metadata.set('x-correlation-id', crypto.randomUUID());
metadata.set('x-tenant-id', 'tenant-abc');

// Valores binários (sufixo -bin obrigatório)
metadata.set('x-request-signature-bin', Buffer.from(signature));

// Passar na chamada
client.getOrder({ id }, metadata, callback);

// Ou com options
client.getOrder({ id }, { metadata, deadline }, callback);

Receber Metadata no Servidor

function getOrderHandler(call, callback) {
  // Ler metadata da requisição
  const authHeader = call.metadata.get('authorization')[0]; // retorna array
  const correlationId = call.metadata.get('x-correlation-id')[0];
  const tenantId = call.metadata.get('x-tenant-id')[0];

  if (!authHeader?.startsWith('Bearer ')) {
    return callback({ code: grpc.status.UNAUTHENTICATED, message: 'Missing token' });
  }

  // ...handler logic
}

Metadata de Resposta: Initial vs Trailing

// Servidor — Initial metadata (antes do response, para headers)
function getOrderHandler(call, callback) {
  const initialMeta = new grpc.Metadata();
  initialMeta.set('x-cache-status', 'MISS');
  initialMeta.set('x-region', 'us-east-1');
  call.sendMetadata(initialMeta); // deve ser chamado ANTES de callback()

  // ...
  callback(null, order);
  // Trailing metadata (após response) é passada como 3º arg do callback:
  // callback(null, order, trailingMeta);
}

// Cliente — receber initial e trailing metadata
const call = client.getOrder({ id }, metadata, (err, response) => {
  // response disponível aqui
});

call.on('metadata', (meta) => {
  console.log('Initial metadata:', meta.get('x-cache-status'));
});

call.on('status', (status) => {
  console.log('Trailing metadata:', status.metadata);
});

Interceptors

Client Interceptor — Logging + Auth + Retry

// Interceptor de logging
function loggingInterceptor(options, nextCall) {
  const start = Date.now();
  const method = options.method_definition.path;

  return new grpc.InterceptingCall(nextCall(options), {
    start(metadata, listener, next) {
      next(metadata, {
        ...listener,
        onReceiveStatus(status, next) {
          const ms = Date.now() - start;
          console.log(`gRPC ${method} → ${status.code} (${ms}ms)`);
          next(status);
        },
      });
    },
  });
}

// Interceptor de auth — injeta token em todas as chamadas
function authInterceptor(tokenProvider) {
  return (options, nextCall) => {
    return new grpc.InterceptingCall(nextCall(options), {
      start(metadata, listener, next) {
        const token = tokenProvider.getToken();
        metadata.set('authorization', `Bearer ${token}`);
        next(metadata, listener);
      },
    });
  };
}

// Encadeamento de interceptors
const client = new OrderServiceClient('host:50051', credentials, {
  interceptors: [loggingInterceptor, authInterceptor(tokenProvider)],
});

Server Interceptor — Auth + Error Handling

// Middleware-like para servidor (via generic-interceptors ou manual)
function serverAuthInterceptor(call, callback, next) {
  const token = call.metadata.get('authorization')[0];

  if (!token) {
    return callback({ code: grpc.status.UNAUTHENTICATED, message: 'Token required' });
  }

  try {
    const payload = jwt.verify(token.replace('Bearer ', ''), process.env.JWT_SECRET);
    call.user = payload; // anexa ao call para handlers downstream
    next(call, callback);
  } catch (err) {
    callback({ code: grpc.status.UNAUTHENTICATED, message: 'Invalid token' });
  }
}

// Wrapper de handler com interceptor
function withAuth(handler) {
  return (call, callback) => {
    serverAuthInterceptor(call, callback, handler);
  };
}

server.addService(OrderService.service, {
  getOrder: withAuth(getOrderHandler),
  createOrder: withAuth(createOrderHandler),
});

Interceptor JWT Completo (Client)

function jwtInterceptor(authClient) {
  return (options, nextCall) => {
    let savedMetadata;
    let savedSendMessage;

    return new grpc.InterceptingCall(nextCall(options), {
      start(metadata, listener, next) {
        savedMetadata = metadata;
        next(metadata, {
          ...listener,
          onReceiveStatus(status, next) {
            if (status.code === grpc.status.UNAUTHENTICATED) {
              // Token expirado — refresh e retry
              authClient.refreshToken().then(newToken => {
                savedMetadata.set('authorization', `Bearer ${newToken}`);
                // Iniciar nova chamada (simplificado; use grpc-retry para produção)
                console.warn('Token refreshed, retry required');
              });
            }
            next(status);
          },
        });
      },
      sendMessage(message, next) {
        savedSendMessage = message;
        next(message);
      },
    });
  };
}

Autenticação

TLS — Channel Credentials

import * as grpc from '@grpc/grpc-js';
import * as fs from 'fs';

// Servidor com TLS
const serverCredentials = grpc.ServerCredentials.createSsl(
  fs.readFileSync('./certs/ca.crt'),   // CA para verificar clientes (mTLS)
  [{
    cert_chain: fs.readFileSync('./certs/server.crt'),
    private_key: fs.readFileSync('./certs/server.key'),
  }],
  true // checkClientCertificate (mTLS)
);

server.bindAsync('0.0.0.0:50051', serverCredentials, callback);

// Cliente com TLS
const channelCredentials = grpc.credentials.createSsl(
  fs.readFileSync('./certs/ca.crt'),
  fs.readFileSync('./certs/client.key'),  // mTLS
  fs.readFileSync('./certs/client.crt'),  // mTLS
);

const client = new OrderServiceClient('orders:50051', channelCredentials);

Token-Based — Call Credentials (por chamada)

// Call credentials combinam com channel credentials
function createTokenCredentials(tokenProvider) {
  return grpc.credentials.createFromMetadataGenerator((params, callback) => {
    const token = tokenProvider.getToken();
    const metadata = new grpc.Metadata();
    metadata.set('authorization', `Bearer ${token}`);
    callback(null, metadata);
  });
}

// Combinar TLS + token por chamada
const channelCreds = grpc.credentials.createSsl(caCert);
const callCreds = createTokenCredentials(myTokenProvider);
const combinedCreds = grpc.credentials.combineChannelCredentials(channelCreds, callCreds);

const client = new OrderServiceClient('orders:50051', combinedCreds);
// Token é injetado automaticamente em cada chamada

gRPC + Service Mesh (Istio/Envoy)

Em ambientes com Istio ou Envoy sidecar:

  • mTLS entre serviços é gerenciado pelo mesh (transparente para a aplicação)
  • grpc.credentials.createInsecure() é seguro dentro do mesh
  • Authn/Authz pode ser delegado ao sidecar via JWT validation policy
  • Sem necessidade de interceptors de auth no código da aplicação
  • Use PeerAuthentication (Istio) para forçar mTLS no namespace
# Istio — forçar mTLS no namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT

gRPC vs REST vs GraphQL

CritériogRPCRESTGraphQL
TransporteHTTP/2HTTP/1.1 ou 2HTTP/1.1 ou 2
PayloadProtobuf (binário)JSON/XMLJSON
TipagemForte (proto obrigatório)Fraca (OpenAPI opcional)Forte (schema obrigatório)
StreamingNativo (4 tipos)SSE / WebSocketSubscriptions
BrowserLimitado (grpc-web)TotalTotal
CachingSem cache HTTPCache nativo (GET)Cache por query hash
Code GenNativo e oficialOpcional (OpenAPI)Opcional
Over/UnderfetchDefinido por métodoOverfetch comumResolvido pelo cliente
Ferramentas de debuggrpcurl, grpc-ui, Evanscurl, Postman, browserGraphiQL, Apollo Studio
Learning curveAlta (proto, protoc)BaixaMédia
Use case idealMicroservices internos, streamingAPIs públicas, web/mobileBFF, queries flexíveis
VersioningCampo reserved, packagesURI versioning (/v2/)Schema evolution
InteropExige protoc em todos os clientesUniversalUniversal

Ferramentas

protoc e Plugins

# Instalar protoc (Ubuntu)
apt install protobuf-compiler

# Instalar plugins Node.js
npm install -g grpc-tools ts-proto

# Gerar código grpc-js + TypeScript
protoc \
  --plugin=protoc-gen-ts_proto=$(which protoc-gen-ts_proto) \
  --ts_proto_out=./src/generated \
  --ts_proto_opt=outputServices=grpc-js,esModuleInterop=true,stringEnums=true \
  --proto_path=./proto \
  $(find ./proto -name "*.proto")

grpcurl — curl para gRPC

# Instalar
brew install grpcurl
# ou: go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest

# Listar serviços (requer reflection ativo no servidor)
grpcurl -plaintext localhost:50051 list

# Listar métodos de um serviço
grpcurl -plaintext localhost:50051 list orders.v1.OrderService

# Chamar método unary
grpcurl -plaintext \
  -H 'authorization: Bearer <token>' \
  -d '{"id": "order-123"}' \
  localhost:50051 orders.v1.OrderService/GetOrder

# Usar arquivo .proto diretamente (sem reflection)
grpcurl -plaintext \
  -import-path ./proto \
  -proto orders/v1/orders.proto \
  -d '{"id": "order-123"}' \
  localhost:50051 orders.v1.OrderService/GetOrder

grpc-ui — Postman para gRPC

# Instalar
go install github.com/fullstorydev/grpcui/cmd/grpcui@latest

# Iniciar interface web (requer reflection)
grpcui -plaintext localhost:50051
# Abre em http://127.0.0.1:PORT — UI para invocar métodos, ver schema

Evans CLI

# Instalar
brew tap ktr0731/evans && brew install evans

# REPL interativo
evans --host localhost --port 50051 --reflection repl

# Modo CLI
evans --host localhost --port 50051 --reflection cli call orders.v1.OrderService.GetOrder

Buf — Lint, Breaking Changes e BSR

# Instalar
brew install bufbuild/buf/buf

# Inicializar
buf config init   # cria buf.yaml

# Lint dos .proto
buf lint

# Detectar breaking changes em relação ao commit anterior
buf breaking --against '.git#branch=main'

# Gerar código (substitui protoc direto)
buf generate

# Push para Buf Schema Registry (BSR)
buf push
# buf.yaml
version: v2
modules:
  - path: proto
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

Reflection Service no Servidor (Node.js)

import { addReflection } from 'grpc-server-reflection';
import { loadFileDescriptorSetFromFile } from '@grpc/proto-loader';

// Habilita grpcurl/grpc-ui sem --proto flag
addReflection(server, './proto/descriptor.bin');
// descriptor.bin gerado via: protoc --descriptor_set_out=descriptor.bin ...