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ério | gRPC | REST | GraphQL |
|---|---|---|---|
| Protocolo | HTTP/2 | HTTP/1.1 ou 2 | HTTP/1.1 ou 2 |
| Formato | Protocol Buffers (binário) | JSON/XML | JSON |
| Contrato | .proto (obrigatório) | OpenAPI (opcional) | Schema GraphQL |
| Streaming | Nativo (4 tipos) | SSE / WebSocket | Subscriptions |
| Browser | Limitado (grpc-web) | Total | Total |
| Code Gen | Nativo | Opcional | Opcional |
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
| Proto3 | JavaScript | Notas |
|---|---|---|
double | number | 64-bit float |
float | number | 32-bit float |
int32 | number | Varlength, negativo ineficiente |
int64 | string/Long | JS não suporta int64 nativo |
uint32 | number | Sem sinal |
uint64 | string/Long | Sem sinal |
sint32 | number | ZigZag, melhor para negativos |
bool | boolean | |
string | string | UTF-8 |
bytes | Buffer | Dados 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-toolsGeraçã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.protoCriar 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ódigo | Nome | HTTP equiv. | Quando usar |
|---|---|---|---|
| 0 | OK | 200 | Sucesso |
| 1 | CANCELLED | 499 | Cancelado pelo cliente |
| 2 | UNKNOWN | 500 | Erro desconhecido |
| 3 | INVALID_ARGUMENT | 400 | Parâmetro inválido na requisição |
| 4 | DEADLINE_EXCEEDED | 504 | Timeout antes da conclusão |
| 5 | NOT_FOUND | 404 | Recurso não encontrado |
| 6 | ALREADY_EXISTS | 409 | Tentativa de criar recurso duplicado |
| 7 | PERMISSION_DENIED | 403 | Sem permissão |
| 8 | RESOURCE_EXHAUSTED | 429 | Rate limit ou quota esgotada |
| 9 | FAILED_PRECONDITION | 400 | Estado inválido para operação |
| 10 | ABORTED | 409 | Conflito de concorrência (ex: CAS) |
| 11 | OUT_OF_RANGE | 400 | Valor fora do intervalo válido |
| 12 | UNIMPLEMENTED | 501 | Método não implementado |
| 13 | INTERNAL | 500 | Erro interno do servidor |
| 14 | UNAVAILABLE | 503 | Serviço temporariamente indisponível |
| 15 | DATA_LOSS | 500 | Dados corrompidos ou perdidos |
| 16 | UNAUTHENTICATED | 401 | Sem 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 chamadagRPC + 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: STRICTgRPC vs REST vs GraphQL
| Critério | gRPC | REST | GraphQL |
|---|---|---|---|
| Transporte | HTTP/2 | HTTP/1.1 ou 2 | HTTP/1.1 ou 2 |
| Payload | Protobuf (binário) | JSON/XML | JSON |
| Tipagem | Forte (proto obrigatório) | Fraca (OpenAPI opcional) | Forte (schema obrigatório) |
| Streaming | Nativo (4 tipos) | SSE / WebSocket | Subscriptions |
| Browser | Limitado (grpc-web) | Total | Total |
| Caching | Sem cache HTTP | Cache nativo (GET) | Cache por query hash |
| Code Gen | Nativo e oficial | Opcional (OpenAPI) | Opcional |
| Over/Underfetch | Definido por método | Overfetch comum | Resolvido pelo cliente |
| Ferramentas de debug | grpcurl, grpc-ui, Evans | curl, Postman, browser | GraphiQL, Apollo Studio |
| Learning curve | Alta (proto, protoc) | Baixa | Média |
| Use case ideal | Microservices internos, streaming | APIs públicas, web/mobile | BFF, queries flexíveis |
| Versioning | Campo reserved, packages | URI versioning (/v2/) | Schema evolution |
| Interop | Exige protoc em todos os clientes | Universal | Universal |
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/GetOrdergrpc-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 schemaEvans 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.GetOrderBuf — 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:
- DEFAULTReflection 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 ...