gRPC e tRPC

gRPC e tRPC

RPC (Remote Procedure Call) é a ideia de invocar uma função num processo remoto como se fosse local. O conceito existe desde os anos 80, mas as implementações modernas — gRPC do Google e tRPC do ecossistema TypeScript — resolvem problemas muito diferentes com abordagens radicalmente distintas. gRPC foca em performance e interoperabilidade polyglot via Protocol Buffers e HTTP/2. tRPC foca em developer experience e type safety end-to-end dentro de monorepos TypeScript, sem nenhuma camada de serialização ou code generation.


1. Protocol Buffers

Protocol Buffers (protobuf) é o formato de serialização binária criado pelo Google. É a lingua franca do gRPC — define tanto a estrutura dos dados quanto a interface dos serviços.

1.1 Sintaxe proto3

syntax = "proto3";

package ecommerce.v1;

option go_package = "github.com/myorg/ecommerce/gen/go/ecommerce/v1";

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

// Enum — valores devem começar em 0
enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;  // Sempre ter um valor zero como "unset"
  ORDER_STATUS_PENDING     = 1;
  ORDER_STATUS_CONFIRMED   = 2;
  ORDER_STATUS_SHIPPED     = 3;
  ORDER_STATUS_DELIVERED   = 4;
  ORDER_STATUS_CANCELLED   = 5;
}

enum PaymentMethod {
  PAYMENT_METHOD_UNSPECIFIED = 0;
  PAYMENT_METHOD_CREDIT_CARD = 1;
  PAYMENT_METHOD_PIX         = 2;
  PAYMENT_METHOD_BOLETO      = 3;
}

// Message principal — Product
message Product {
  string id          = 1;
  string name        = 2;
  string description = 3;
  int32  price_cents = 4;   // Preço em centavos — nunca use float para dinheiro
  string category    = 5;
  repeated string tags = 6; // Lista de strings
  map<string, string> metadata = 7; // Map genérico
  ProductDetails details = 8;
  google.protobuf.Timestamp created_at = 9;
}

// Oneof — apenas um dos campos pode estar setado
message ProductDetails {
  oneof kind {
    PhysicalProduct physical = 1;
    DigitalProduct  digital  = 2;
  }
}

message PhysicalProduct {
  double weight_kg = 1;
  Dimensions dimensions = 2;
}

message Dimensions {
  double height_cm = 1;
  double width_cm  = 2;
  double depth_cm  = 3;
}

message DigitalProduct {
  string download_url = 1;
  int64  file_size_bytes = 2;
}

// Order com campos compostos
message Order {
  string id                     = 1;
  string customer_id            = 2;
  repeated OrderItem items      = 3;
  OrderStatus status            = 4;
  int64 total_cents             = 5;
  PaymentMethod payment_method  = 6;
  Address shipping_address      = 7;
  google.protobuf.Timestamp created_at  = 8;
  google.protobuf.Timestamp updated_at  = 9;
  google.protobuf.StringValue coupon_code = 10; // Wrapper para nullable string

  // Reserved — campos removidos que não podem ser reutilizados
  reserved 11, 12;
  reserved "legacy_discount", "old_status";
}

message OrderItem {
  string product_id = 1;
  int32  quantity   = 2;
  int64  unit_price_cents = 3;
}

message Address {
  string street  = 1;
  string city    = 2;
  string state   = 3;
  string zip     = 4;
  string country = 5;
}

1.2 Tipos Escalares e Wire Types

Protobuf usa wire types para codificação binária. Cada tipo escalar mapeia para um wire type:

Wire Type   Formato            Tipos Proto
─────────   ─────────────────  ──────────────────────────────
0           Varint             int32, int64, uint32, uint64,
                               sint32, sint64, bool, enum
1           64-bit             fixed64, sfixed64, double
2           Length-delimited   string, bytes, messages,
                               repeated (packed)
5           32-bit             fixed32, sfixed32, float

Ponto importante: int32 e int64 usam varint encoding, que é eficiente para números positivos pequenos mas ineficiente para negativos (usa 10 bytes para -1). Para campos que frequentemente contêm negativos, use sint32/sint64 que aplicam ZigZag encoding.

1.3 Field Numbers e Evolução de Schema

Os field numbers são a peça central da compatibilidade. O nome do campo é irrelevante no wire format — apenas o número importa.

Regras de backward/forward compatibility:

SEGURO (não causa breaking change):
  ✓ Adicionar novo campo (com novo número)
  ✓ Remover campo (marcar como reserved)
  ✓ Renomear campo (número permanece)
  ✓ Mudar int32 ↔ int64, uint32 ↔ uint64 (compatíveis no wire)
  ✓ Mudar string ↔ bytes (se bytes for UTF-8 válido)

BREAKING (causa incompatibilidade):
  ✗ Mudar field number de um campo existente
  ✗ Mudar wire type (ex: int32 → string)
  ✗ Reutilizar field number de campo removido
  ✗ Mudar tipo de repeated ↔ scalar
  ✗ Mudar required ↔ optional (proto2)

Reserved fields protegem contra reutilização acidental:

message User {
  string id = 1;
  string email = 2;
  // Campo 3 era "password_hash" — removido por segurança
  // Campo 4 era "legacy_role" — migrado para RBAC
  reserved 3, 4;
  reserved "password_hash", "legacy_role";
  string name = 5;
}

1.4 Compilação com protoc

# Instalar protoc
brew install protobuf  # macOS

# Plugins de linguagem
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
npm install -g ts-proto  # Plugin TypeScript

# Compilar para Go
protoc \
  --go_out=./gen/go --go_opt=paths=source_relative \
  --go-grpc_out=./gen/go --go-grpc_opt=paths=source_relative \
  proto/ecommerce/v1/*.proto

# Compilar para TypeScript (ts-proto)
protoc \
  --plugin=./node_modules/.bin/protoc-gen-ts_proto \
  --ts_proto_out=./gen/ts \
  --ts_proto_opt=outputServices=grpc-js \
  --ts_proto_opt=esModuleInterop=true \
  proto/ecommerce/v1/*.proto

2. gRPC Fundamentals

gRPC é um framework RPC open-source criado pelo Google em 2015. Usa HTTP/2 como transporte e Protocol Buffers como serialização por default.

2.1 Arquitetura

┌─────────────────────────────────────────────────────────────┐
│                        gRPC CLIENT                          │
│                                                             │
│  Application Code                                           │
│       │                                                     │
│       ▼                                                     │
│  Generated Stub ──→ Serializa (protobuf) ──→ Channel        │
│                                                │            │
│                          Interceptors ◄────────┤            │
│                          (middleware)          │            │
│                                                │            │
│                          HTTP/2 Connection ◄───┘            │
└───────────────────────────┬─────────────────────────────────┘
                            │ HTTP/2 stream
                            │ (HEADERS + DATA frames)

┌─────────────────────────────────────────────────────────────┐
│                        gRPC SERVER                          │
│                                                             │
│  HTTP/2 Connection                                          │
│       │                                                     │
│       ▼                                                     │
│  Interceptors ──→ Deserializa (protobuf) ──→ Service Impl   │
│  (middleware)                                     │         │
│                                                   ▼         │
│                                            Handler Method   │
│                                                   │         │
│                    Serializa response ◄────────────┘         │
│                         │                                   │
│                         ▼                                   │
│                    HTTP/2 Response                           │
│                    (DATA + TRAILERS)                         │
└─────────────────────────────────────────────────────────────┘

2.2 Os 4 Tipos de RPC

Unary RPC (Request-Response)

O padrão mais simples — um request, um response. Equivalente a uma chamada REST.

service ProductService {
  rpc GetProduct(GetProductRequest) returns (GetProductResponse);
}

message GetProductRequest {
  string id = 1;
}

message GetProductResponse {
  Product product = 1;
}

Implementação em Go (server):

func (s *productServer) GetProduct(
    ctx context.Context,
    req *pb.GetProductRequest,
) (*pb.GetProductResponse, error) {
    // Deadline propagation — verificar se o contexto já expirou
    if ctx.Err() == context.DeadlineExceeded {
        return nil, status.Error(codes.DeadlineExceeded, "deadline exceeded")
    }

    product, err := s.repo.FindByID(ctx, req.GetId())
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            return nil, status.Error(codes.NotFound, "product not found")
        }
        return nil, status.Error(codes.Internal, "internal error")
    }

    return &pb.GetProductResponse{
        Product: toProtoProduct(product),
    }, nil
}

Implementação em TypeScript (client):

import { ProductServiceClient } from './gen/ecommerce/v1/product_grpc_pb';
import { GetProductRequest } from './gen/ecommerce/v1/product_pb';
import { credentials, Metadata } from '@grpc/grpc-js';

const client = new ProductServiceClient(
  'localhost:50051',
  credentials.createInsecure()
);

async function getProduct(id: string): Promise<Product> {
  const request = new GetProductRequest();
  request.setId(id);

  const metadata = new Metadata();
  metadata.set('x-request-id', crypto.randomUUID());

  return new Promise((resolve, reject) => {
    client.getProduct(request, metadata, (err, response) => {
      if (err) return reject(err);
      resolve(response!.getProduct()!.toObject());
    });
  });
}

Server-Streaming RPC

O servidor envia múltiplas mensagens em resposta a um único request. Ideal para feeds, logs em tempo real, ou resultados paginados como stream.

service OrderService {
  // Cliente pede atualizações de um pedido — servidor envia stream de status
  rpc TrackOrder(TrackOrderRequest) returns (stream OrderUpdate);
}

message TrackOrderRequest {
  string order_id = 1;
}

message OrderUpdate {
  string order_id = 1;
  OrderStatus status = 2;
  string message = 3;
  google.protobuf.Timestamp timestamp = 4;
}

Implementação em Go (server):

func (s *orderServer) TrackOrder(
    req *pb.TrackOrderRequest,
    stream pb.OrderService_TrackOrderServer,
) error {
    orderID := req.GetOrderId()

    // Subscrever a um canal de eventos (ex: Redis Pub/Sub, Kafka)
    updates, cancel := s.eventBus.Subscribe(orderID)
    defer cancel()

    for {
        select {
        case <-stream.Context().Done():
            // Cliente desconectou ou deadline expirou
            return stream.Context().Err()
        case update, ok := <-updates:
            if !ok {
                // Canal fechado — pedido finalizado
                return nil
            }
            if err := stream.Send(update); err != nil {
                return status.Errorf(codes.Internal, "send failed: %v", err)
            }
        }
    }
}

Client-Streaming RPC

O cliente envia múltiplas mensagens e o servidor responde com uma única mensagem após receber todas. Ideal para uploads em chunks, ou envio de batch de dados.

service AnalyticsService {
  // Cliente envia stream de eventos, servidor responde com resumo
  rpc IngestEvents(stream AnalyticsEvent) returns (IngestSummary);
}

message AnalyticsEvent {
  string event_type = 1;
  string user_id = 2;
  map<string, string> properties = 3;
  google.protobuf.Timestamp timestamp = 4;
}

message IngestSummary {
  int64 events_received = 1;
  int64 events_processed = 2;
  int64 events_failed = 3;
}

Implementação em Go (server):

func (s *analyticsServer) IngestEvents(
    stream pb.AnalyticsService_IngestEventsServer,
) error {
    var received, processed, failed int64

    for {
        event, err := stream.Recv()
        if err == io.EOF {
            // Cliente terminou de enviar — retornar resumo
            return stream.SendAndClose(&pb.IngestSummary{
                EventsReceived:  received,
                EventsProcessed: processed,
                EventsFailed:    failed,
            })
        }
        if err != nil {
            return status.Errorf(codes.Internal, "recv error: %v", err)
        }

        received++
        if err := s.processor.Process(stream.Context(), event); err != nil {
            failed++
            continue
        }
        processed++
    }
}

Bidirectional Streaming RPC

Ambos os lados enviam streams independentes. Os dois streams operam de forma independente — o servidor não precisa esperar o cliente terminar antes de responder.

service ChatService {
  // Chat em tempo real — ambos enviam e recebem mensagens
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

message ChatMessage {
  string sender_id = 1;
  string room_id = 2;
  string content = 3;
  google.protobuf.Timestamp timestamp = 4;
}

Implementação em Go (server):

func (s *chatServer) Chat(stream pb.ChatService_ChatServer) error {
    // Extrair metadata com info do user
    md, ok := metadata.FromIncomingContext(stream.Context())
    if !ok {
        return status.Error(codes.Unauthenticated, "missing metadata")
    }
    userID := md.Get("x-user-id")[0]

    // Registrar este stream no room
    room := s.rooms.Join(userID, stream)
    defer s.rooms.Leave(userID, room)

    for {
        msg, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }

        // Broadcast para todos os outros participantes do room
        room.Broadcast(msg, userID)
    }
}

2.3 Fluxo HTTP/2 — Unary RPC

Cliente                                      Servidor
   │                                            │
   │─── HEADERS frame ─────────────────────────▶│
   │    :method: POST                           │
   │    :path: /ecommerce.v1.ProductService/    │
   │           GetProduct                       │
   │    content-type: application/grpc          │
   │    te: trailers                            │
   │    grpc-timeout: 5S                        │
   │                                            │
   │─── DATA frame (Length-Prefixed Message) ──▶│
   │    [1 byte: compressed?] [4 bytes: length] │
   │    [N bytes: protobuf-encoded request]     │
   │                                            │
   │─── END_STREAM ────────────────────────────▶│
   │                                            │
   │                                            │ (processa)
   │                                            │
   │◀── HEADERS frame ─────────────────────────│
   │    :status: 200                            │
   │    content-type: application/grpc          │
   │                                            │
   │◀── DATA frame (Length-Prefixed Message) ──│
   │    [protobuf-encoded response]             │
   │                                            │
   │◀── HEADERS frame (trailers) ──────────────│
   │    grpc-status: 0                          │
   │    grpc-message: OK                        │
   │                                            │

Ponto chave: o gRPC status code viaja nos trailers (HEADERS frame final com END_STREAM), não no HTTP status code. O :status: 200 na resposta HTTP é quase sempre 200 — o real status da RPC está no grpc-status trailer.


3. gRPC Internals

3.1 HTTP/2 Multiplexing

HTTP/2 permite múltiplas RPCs simultâneas na mesma conexão TCP via streams. Cada RPC é um stream independente com seu próprio ID.

Conexão TCP (TLS)
┌─────────────────────────────────────────────────┐
│                                                 │
│  Stream 1 ──▶ GetProduct(id=123)       ◀── OK  │
│  Stream 3 ──▶ ListOrders(user=abc)     ◀── ... │
│  Stream 5 ──▶ TrackOrder(id=456)       ◀── ... │
│  Stream 7 ──▶ GetProduct(id=789)       ◀── OK  │
│                                                 │
│  Todos os streams compartilham a mesma conexão  │
│  TCP e são multiplexados em frames              │
│                                                 │
└─────────────────────────────────────────────────┘

Isto elimina o head-of-line blocking do HTTP/1.1, onde um response lento bloqueava todos os requests subsequentes na mesma conexão.

3.2 Flow Control

HTTP/2 implementa flow control por stream e por conexão via WINDOW_UPDATE frames. O receptor anuncia quantos bytes está disposto a receber. Quando a janela esgota, o emissor para de enviar até receber um WINDOW_UPDATE.

Em gRPC, isto é configurável:

server := grpc.NewServer(
    grpc.InitialWindowSize(1 << 20),     // 1MB por stream
    grpc.InitialConnWindowSize(1 << 20), // 1MB por conexão
)

3.3 Deadline Propagation

Um dos mecanismos mais poderosos do gRPC. Quando um cliente define um deadline, este propaga-se automaticamente para todos os serviços downstream via o header grpc-timeout.

API Gateway          Serviço A           Serviço B          Serviço C
    │                    │                   │                   │
    │─ deadline: 5s ────▶│                   │                   │
    │                    │─ deadline: 4.8s ─▶│                   │
    │                    │                   │─ deadline: 4.5s ─▶│
    │                    │                   │                   │
    │  Se C demorar mais de 4.5s, toda a cadeia falha com       │
    │  DEADLINE_EXCEEDED, evitando trabalho desnecessário        │

Em Go, o deadline propaga via context:

func (s *serviceA) ProcessOrder(ctx context.Context, req *pb.Request) (*pb.Response, error) {
    // O deadline do contexto já inclui o timeout recebido do caller
    deadline, ok := ctx.Deadline()
    if ok {
        log.Printf("deadline restante: %v", time.Until(deadline))
    }

    // Ao chamar Serviço B, o deadline propaga automaticamente
    resp, err := s.serviceBClient.Validate(ctx, &pb.ValidateRequest{...})
    if err != nil {
        st, _ := status.FromError(err)
        if st.Code() == codes.DeadlineExceeded {
            // Timeout propagado — não adianta retry
            return nil, err
        }
    }
    return resp, nil
}

3.4 Interceptors

Interceptors são o equivalente a middleware no gRPC. Existem dois tipos: unary e stream.

Unary interceptor em Go (logging + metrics):

func loggingUnaryInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    start := time.Now()

    // Extrair metadata
    md, _ := metadata.FromIncomingContext(ctx)
    requestID := md.Get("x-request-id")

    // Chamar o handler real
    resp, err := handler(ctx, req)

    // Log com duração e status
    duration := time.Since(start)
    st, _ := status.FromError(err)

    log.Printf(
        "method=%s request_id=%s duration=%v status=%s",
        info.FullMethod, requestID, duration, st.Code(),
    )

    // Métricas
    grpcRequestDuration.WithLabelValues(info.FullMethod, st.Code().String()).
        Observe(duration.Seconds())

    return resp, err
}

// Registrar no server
server := grpc.NewServer(
    grpc.ChainUnaryInterceptor(
        loggingUnaryInterceptor,
        authUnaryInterceptor,
        recoveryUnaryInterceptor,
    ),
)

Stream interceptor em Go (auth):

func authStreamInterceptor(
    srv interface{},
    ss grpc.ServerStream,
    info *grpc.StreamServerInfo,
    handler grpc.StreamHandler,
) error {
    md, ok := metadata.FromIncomingContext(ss.Context())
    if !ok {
        return status.Error(codes.Unauthenticated, "missing metadata")
    }

    tokens := md.Get("authorization")
    if len(tokens) == 0 {
        return status.Error(codes.Unauthenticated, "missing token")
    }

    claims, err := validateJWT(tokens[0])
    if err != nil {
        return status.Error(codes.Unauthenticated, "invalid token")
    }

    // Injetar claims no context
    ctx := context.WithValue(ss.Context(), claimsKey, claims)
    wrapped := &wrappedStream{ServerStream: ss, ctx: ctx}

    return handler(srv, wrapped)
}

3.5 Metadata

Metadata em gRPC é equivalente a HTTP headers. Propagada via o contexto, permite passar informação transversal como request IDs, tokens, tenant IDs.

// Client — enviar metadata
md := metadata.New(map[string]string{
    "x-request-id": uuid.New().String(),
    "x-tenant-id":  "tenant-123",
    "authorization": "Bearer " + token,
})
ctx := metadata.NewOutgoingContext(ctx, md)
resp, err := client.GetProduct(ctx, req)

// Client — ler trailers da resposta
var trailer metadata.MD
resp, err := client.GetProduct(ctx, req, grpc.Trailer(&trailer))
retryAfter := trailer.Get("x-retry-after")

// Server — ler metadata recebida
md, ok := metadata.FromIncomingContext(ctx)
tenantID := md.Get("x-tenant-id")[0]

// Server — enviar metadata na resposta (headers)
header := metadata.New(map[string]string{"x-served-by": hostname})
grpc.SendHeader(ctx, header)

// Server — enviar trailers
trailer := metadata.New(map[string]string{"x-request-cost": "42"})
grpc.SetTrailer(ctx, trailer)

4. gRPC em Produção

4.1 Health Checking

O gRPC define um protocolo standard de health check (grpc.health.v1.Health):

// Já definido pelo gRPC — não precisa criar
service Health {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
  rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}

message HealthCheckRequest {
  string service = 1;  // "" = overall health
}

message HealthCheckResponse {
  enum ServingStatus {
    UNKNOWN = 0;
    SERVING = 1;
    NOT_SERVING = 2;
    SERVICE_UNKNOWN = 3;
  }
  ServingStatus status = 1;
}
import "google.golang.org/grpc/health"
import healthpb "google.golang.org/grpc/health/grpc_health_v1"

healthServer := health.NewServer()
healthpb.RegisterHealthServer(grpcServer, healthServer)

// Marcar serviço como serving
healthServer.SetServingStatus("ecommerce.v1.ProductService", healthpb.HealthCheckResponse_SERVING)

// Marcar como not serving (durante graceful shutdown)
healthServer.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)

4.2 Reflection e grpcurl

Reflection permite que ferramentas descubram os serviços disponíveis sem ter os .proto files.

import "google.golang.org/grpc/reflection"

reflection.Register(grpcServer)
# Listar serviços
grpcurl -plaintext localhost:50051 list

# Descrever um serviço
grpcurl -plaintext localhost:50051 describe ecommerce.v1.ProductService

# Chamar um método
grpcurl -plaintext \
  -d '{"id": "prod-123"}' \
  localhost:50051 ecommerce.v1.ProductService/GetProduct

# Com metadata
grpcurl -plaintext \
  -H 'authorization: Bearer eyJ...' \
  -H 'x-tenant-id: tenant-456' \
  -d '{"id": "prod-123"}' \
  localhost:50051 ecommerce.v1.ProductService/GetProduct

4.3 Load Balancing

gRPC usa conexões HTTP/2 long-lived — um load balancer L4 (TCP) distribui apenas na criação da conexão, não por request. Isto causa desbalanceamento severo.

PROBLEMA com L4 load balancer:
                                          ┌────────────────┐
              conexão 1 (100 RPCs) ──────▶│   Server A     │ (sobrecarregado)
             ╱                            └────────────────┘
┌────────┐  ╱                             ┌────────────────┐
│ Client │────conexão 2 (100 RPCs) ──────▶│   Server B     │ (sobrecarregado)
└────────┘  ╲                             └────────────────┘
             ╲                            ┌────────────────┐
              conexão 3 (0 RPCs) ────────▶│   Server C     │ (idle)
                                          └────────────────┘

Soluções:

1. Client-side load balancing:

import _ "google.golang.org/grpc/balancer/roundrobin"

conn, err := grpc.Dial(
    "dns:///my-service.default.svc.cluster.local:50051",
    grpc.WithDefaultServiceConfig(`{
        "loadBalancingConfig": [{"round_robin": {}}]
    }`),
    grpc.WithTransportCredentials(insecure.NewCredentials()),
)

2. Proxy-based (Envoy):

# Envoy config — gRPC-aware L7 load balancing
static_resources:
  listeners:
    - name: grpc_listener
      address:
        socket_address: { address: 0.0.0.0, port_value: 8080 }
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                stat_prefix: grpc
                codec_type: AUTO
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: grpc_service
                      domains: ["*"]
                      routes:
                        - match: { prefix: "/" }
                          route:
                            cluster: grpc_backend
                            timeout: 5s
                            retry_policy:
                              retry_on: "unavailable,resource-exhausted"
                              num_retries: 3
                http_filters:
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  clusters:
    - name: grpc_backend
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      typed_extension_protocol_options:
        envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
          "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
          explicit_http_config:
            http2_protocol_options: {}
      load_assignment:
        cluster_name: grpc_backend
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address: { address: grpc-service, port_value: 50051 }

4.4 Error Model

gRPC define status codes explícitos (não reusa HTTP status codes):

Code              Número  Quando Usar
────              ──────  ──────────────────────────────────
OK                0       Sucesso
CANCELLED         1       Cliente cancelou
UNKNOWN           2       Erro desconhecido
INVALID_ARGUMENT  3       Input inválido (validação)
DEADLINE_EXCEEDED 4       Timeout (não fazer retry)
NOT_FOUND         5       Recurso não encontrado
ALREADY_EXISTS    6       Conflito de criação
PERMISSION_DENIED 7       Sem permissão (autenticado mas não autorizado)
RESOURCE_EXHAUSTED 8      Rate limit, quota excedida
UNIMPLEMENTED     12      Método não implementado
INTERNAL          13      Erro interno do servidor
UNAVAILABLE       14      Serviço temporariamente indisponível (fazer retry)
UNAUTHENTICATED   16      Não autenticado (token inválido ou ausente)

Rich error details (Google error model):

import (
    "google.golang.org/genproto/googleapis/rpc/errdetails"
    "google.golang.org/grpc/status"
)

func validateCreateOrder(req *pb.CreateOrderRequest) error {
    var violations []*errdetails.BadRequest_FieldViolation

    if len(req.Items) == 0 {
        violations = append(violations, &errdetails.BadRequest_FieldViolation{
            Field:       "items",
            Description: "pedido deve ter pelo menos um item",
        })
    }

    if req.ShippingAddress == nil {
        violations = append(violations, &errdetails.BadRequest_FieldViolation{
            Field:       "shipping_address",
            Description: "endereço de entrega obrigatório",
        })
    }

    if len(violations) > 0 {
        st := status.New(codes.InvalidArgument, "validação falhou")
        detailed, _ := st.WithDetails(&errdetails.BadRequest{
            FieldViolations: violations,
        })
        return detailed.Err()
    }
    return nil
}

4.5 Retry Policy e Hedging

Configuração de retry via service config (declarativo, sem código):

conn, err := grpc.Dial(target,
    grpc.WithDefaultServiceConfig(`{
        "methodConfig": [{
            "name": [{"service": "ecommerce.v1.ProductService"}],
            "retryPolicy": {
                "maxAttempts": 4,
                "initialBackoff": "0.1s",
                "maxBackoff": "1s",
                "backoffMultiplier": 2.0,
                "retryableStatusCodes": ["UNAVAILABLE", "RESOURCE_EXHAUSTED"]
            }
        }]
    }`),
)

Hedging envia múltiplos requests simultâneos e usa a primeira resposta. Bom para latência tail (p99) quando o custo de requests extras é aceitável:

{
  "methodConfig": [{
    "name": [{"service": "ecommerce.v1.ProductService", "method": "GetProduct"}],
    "hedgingPolicy": {
      "maxAttempts": 3,
      "hedgingDelay": "0.5s",
      "nonFatalStatusCodes": ["UNAVAILABLE"]
    }
  }]
}

5. tRPC

tRPC é uma abordagem completamente diferente: em vez de definir schemas externos (protobuf, OpenAPI, GraphQL SDL), o type system do TypeScript é o schema. O tipo flui do backend para o frontend sem code generation, sem serialização manual, sem runtime overhead de validação duplicada.

5.1 Arquitetura

┌────────────────────────────────────────────────────────┐
│                     MONOREPO                           │
│                                                        │
│  ┌─────────────────────────────────────┐               │
│  │  Backend (tRPC Server)              │               │
│  │                                     │               │
│  │  Router                             │               │
│  │    ├─ user.getById (query)          │               │
│  │    ├─ user.create (mutation)        │  AppRouter    │
│  │    ├─ order.list (query)            │──── type ────▶│
│  │    └─ order.create (mutation)       │  (exportado)  │
│  │                                     │               │
│  │  Zod schemas validam input          │               │
│  │  Prisma gera types do DB            │               │
│  └──────────────┬──────────────────────┘               │
│                 │ HTTP / WebSocket                      │
│  ┌──────────────▼──────────────────────┐               │
│  │  Frontend (tRPC Client)             │               │
│  │                                     │               │
│  │  trpc.user.getById.useQuery()       │               │
│  │  trpc.order.create.useMutation()    │               │
│  │                                     │               │
│  │  Autocomplete completo              │               │
│  │  Type errors em compile time        │               │
│  └─────────────────────────────────────┘               │
└────────────────────────────────────────────────────────┘

5.2 Implementação Completa com Next.js App Router

1. Definir o router (server):

// src/server/trpc.ts — Inicialização do tRPC
import { initTRPC, TRPCError } from '@trpc/server';
import { type CreateNextContextOptions } from '@trpc/server/adapters/next';
import superjson from 'superjson';
import { ZodError } from 'zod';
import { prisma } from './db';
import { getServerSession } from './auth';

// Context — disponível em todas as procedures
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
  const session = await getServerSession();
  return {
    prisma,
    session,
    headers: opts.req.headers,
  };
};

const t = initTRPC.context<typeof createTRPCContext>().create({
  transformer: superjson, // Suporta Date, Map, Set, BigInt
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});

export const router = t.router;
export const publicProcedure = t.procedure;
export const createCallerFactory = t.createCallerFactory;

// Middleware de autenticação
const enforceAuth = t.middleware(({ ctx, next }) => {
  if (!ctx.session?.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({
    ctx: {
      session: { ...ctx.session, user: ctx.session.user },
    },
  });
});

export const protectedProcedure = t.procedure.use(enforceAuth);

2. Definir procedures com Zod:

// src/server/routers/order.ts
import { z } from 'zod';
import { router, protectedProcedure } from '../trpc';
import { TRPCError } from '@trpc/server';

// Zod schemas — servem como validação E como tipos
const createOrderInput = z.object({
  items: z.array(z.object({
    productId: z.string().uuid(),
    quantity: z.number().int().min(1).max(100),
  })).min(1, 'Pedido deve ter pelo menos um item'),
  shippingAddress: z.object({
    street: z.string().min(5),
    city: z.string().min(2),
    state: z.string().length(2),
    zip: z.string().regex(/^\d{5}-?\d{3}$/),
  }),
  couponCode: z.string().optional(),
});

const listOrdersInput = z.object({
  cursor: z.string().uuid().optional(),
  limit: z.number().int().min(1).max(50).default(20),
  status: z.enum(['PENDING', 'CONFIRMED', 'SHIPPED', 'DELIVERED']).optional(),
});

export const orderRouter = router({
  // Query — buscar pedidos com cursor pagination
  list: protectedProcedure
    .input(listOrdersInput)
    .query(async ({ ctx, input }) => {
      const orders = await ctx.prisma.order.findMany({
        where: {
          customerId: ctx.session.user.id,
          ...(input.status && { status: input.status }),
        },
        take: input.limit + 1, // +1 para saber se tem próxima página
        ...(input.cursor && {
          cursor: { id: input.cursor },
          skip: 1,
        }),
        orderBy: { createdAt: 'desc' },
        include: {
          items: { include: { product: true } },
        },
      });

      let nextCursor: string | undefined;
      if (orders.length > input.limit) {
        const next = orders.pop();
        nextCursor = next?.id;
      }

      return { orders, nextCursor };
    }),

  // Query — buscar pedido por ID
  getById: protectedProcedure
    .input(z.object({ id: z.string().uuid() }))
    .query(async ({ ctx, input }) => {
      const order = await ctx.prisma.order.findUnique({
        where: { id: input.id },
        include: {
          items: { include: { product: true } },
          shippingAddress: true,
        },
      });

      if (!order) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: `Pedido ${input.id} não encontrado`,
        });
      }

      if (order.customerId !== ctx.session.user.id) {
        throw new TRPCError({ code: 'FORBIDDEN' });
      }

      return order;
    }),

  // Mutation — criar pedido
  create: protectedProcedure
    .input(createOrderInput)
    .mutation(async ({ ctx, input }) => {
      // Buscar produtos e calcular total
      const products = await ctx.prisma.product.findMany({
        where: { id: { in: input.items.map(i => i.productId) } },
      });

      if (products.length !== input.items.length) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Um ou mais produtos não encontrados',
        });
      }

      const totalCents = input.items.reduce((sum, item) => {
        const product = products.find(p => p.id === item.productId)!;
        return sum + product.priceCents * item.quantity;
      }, 0);

      // Criar pedido em transação
      const order = await ctx.prisma.$transaction(async (tx) => {
        const order = await tx.order.create({
          data: {
            customerId: ctx.session.user.id,
            status: 'PENDING',
            totalCents,
            items: {
              create: input.items.map(item => ({
                productId: item.productId,
                quantity: item.quantity,
                unitPriceCents: products.find(p => p.id === item.productId)!.priceCents,
              })),
            },
            shippingAddress: { create: input.shippingAddress },
          },
          include: { items: true },
        });

        // Decrementar stock
        for (const item of input.items) {
          await tx.product.update({
            where: { id: item.productId },
            data: { stock: { decrement: item.quantity } },
          });
        }

        return order;
      });

      return order;
    }),

  // Mutation — cancelar pedido
  cancel: protectedProcedure
    .input(z.object({ id: z.string().uuid() }))
    .mutation(async ({ ctx, input }) => {
      const order = await ctx.prisma.order.findUnique({
        where: { id: input.id },
      });

      if (!order || order.customerId !== ctx.session.user.id) {
        throw new TRPCError({ code: 'NOT_FOUND' });
      }

      if (order.status !== 'PENDING') {
        throw new TRPCError({
          code: 'PRECONDITION_FAILED',
          message: `Pedido com status ${order.status} não pode ser cancelado`,
        });
      }

      return ctx.prisma.order.update({
        where: { id: input.id },
        data: { status: 'CANCELLED' },
      });
    }),
});

3. Root router:

// src/server/routers/_app.ts
import { router } from '../trpc';
import { orderRouter } from './order';
import { productRouter } from './product';
import { userRouter } from './user';

export const appRouter = router({
  order: orderRouter,
  product: productRouter,
  user: userRouter,
});

// Exportar o TYPE — nunca importar o router real no client
export type AppRouter = typeof appRouter;

4. React Query integration (client):

// src/trpc/client.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';

export const trpc = createTRPCReact<AppRouter>();
// src/app/orders/page.tsx
'use client';

import { trpc } from '@/trpc/client';

export default function OrdersPage() {
  // useQuery — type-safe, autocomplete nos campos
  const { data, fetchNextPage, hasNextPage, isLoading } =
    trpc.order.list.useInfiniteQuery(
      { limit: 20, status: 'PENDING' },
      {
        getNextPageParam: (lastPage) => lastPage.nextCursor,
        staleTime: 30_000, // 30 segundos
      },
    );

  // useMutation — optimistic update
  const cancelOrder = trpc.order.cancel.useMutation({
    onMutate: async ({ id }) => {
      // Cancel outgoing refetches
      await utils.order.list.cancel();

      // Snapshot do estado anterior
      const previous = utils.order.list.getInfiniteData({ limit: 20 });

      // Optimistic update
      utils.order.list.setInfiniteData({ limit: 20 }, (old) => {
        if (!old) return old;
        return {
          ...old,
          pages: old.pages.map(page => ({
            ...page,
            orders: page.orders.map(order =>
              order.id === id ? { ...order, status: 'CANCELLED' } : order
            ),
          })),
        };
      });

      return { previous };
    },
    onError: (_err, _vars, context) => {
      // Rollback em caso de erro
      if (context?.previous) {
        utils.order.list.setInfiniteData({ limit: 20 }, context.previous);
      }
    },
    onSettled: () => {
      // Invalidar cache para refetch
      utils.order.list.invalidate();
    },
  });

  const utils = trpc.useUtils();

  if (isLoading) return <div>Carregando...</div>;

  return (
    <div>
      {data?.pages.flatMap(page =>
        page.orders.map(order => (
          <div key={order.id}>
            <p>Pedido {order.id}{order.status}</p>
            <p>Total: R$ {(order.totalCents / 100).toFixed(2)}</p>
            {order.status === 'PENDING' && (
              <button onClick={() => cancelOrder.mutate({ id: order.id })}>
                Cancelar
              </button>
            )}
          </div>
        ))
      )}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()}>Carregar mais</button>
      )}
    </div>
  );
}

5.3 Middleware em tRPC

// Middleware de logging com timing
const timingMiddleware = t.middleware(async ({ path, type, next }) => {
  const start = performance.now();
  const result = await next();
  const duration = performance.now() - start;

  if (duration > 500) {
    console.warn(`[SLOW] ${type} ${path} levou ${duration.toFixed(0)}ms`);
  }

  return result;
});

// Middleware de rate limiting
const rateLimitMiddleware = t.middleware(async ({ ctx, next }) => {
  const ip = ctx.headers.get('x-forwarded-for') ?? 'unknown';
  const key = `ratelimit:${ip}`;

  const current = await redis.incr(key);
  if (current === 1) await redis.expire(key, 60);

  if (current > 100) {
    throw new TRPCError({
      code: 'TOO_MANY_REQUESTS',
      message: 'Rate limit excedido. Tente novamente em 1 minuto.',
    });
  }

  return next();
});

// Compor middlewares
export const rateLimitedProcedure = publicProcedure
  .use(timingMiddleware)
  .use(rateLimitMiddleware);

5.4 Error Handling

import { TRPCError } from '@trpc/server';

// No server — erros tipados
throw new TRPCError({
  code: 'NOT_FOUND',
  message: 'Produto não encontrado',
  cause: originalError, // Encapsular erro original
});

// Códigos disponíveis mapeiam para HTTP status:
// PARSE_ERROR          → 400
// BAD_REQUEST          → 400
// UNAUTHORIZED         → 401
// FORBIDDEN            → 403
// NOT_FOUND            → 404
// METHOD_NOT_SUPPORTED → 405
// TIMEOUT              → 408
// CONFLICT             → 409
// PRECONDITION_FAILED  → 412
// TOO_MANY_REQUESTS    → 429
// INTERNAL_SERVER_ERROR → 500

// No client — tratamento de erros
const createOrder = trpc.order.create.useMutation({
  onError: (error) => {
    if (error.data?.code === 'BAD_REQUEST') {
      // Erros de validação Zod
      const zodErrors = error.data.zodError?.fieldErrors;
      if (zodErrors) {
        Object.entries(zodErrors).forEach(([field, messages]) => {
          form.setError(field, { message: messages?.join(', ') });
        });
      }
    } else if (error.data?.code === 'UNAUTHORIZED') {
      router.push('/login');
    } else {
      toast.error('Erro inesperado. Tente novamente.');
    }
  },
});

6. Decision Matrix: gRPC vs REST vs GraphQL vs tRPC

6.1 Tabela Comparativa

Critério           REST          gRPC          GraphQL       tRPC
───────────────    ──────────    ──────────    ──────────    ──────────
Serialização       JSON          Protobuf      JSON          JSON
                   (texto)       (binário)     (texto)       (superjson)

Latência           Média         Baixa         Média-Alta    Baixa
                                 (HTTP/2 +     (parsing      (zero
                                 binário)      overhead)     overhead)

Type Safety        Manual        Forte         Forte         End-to-end
                   (OpenAPI)     (code gen)    (code gen)    (nativo TS)

Streaming          SSE/WS        Nativo        Subscriptions WebSocket
                   (workaround)  (4 padrões)   (WS)          (limitado)

Browser Support    Nativo        Via proxy     Nativo        Nativo
                                 (grpc-web)

Code Generation    Opcional      Obrigatório   Recomendado   Nenhum
                   (openapi-     (protoc)      (graphql-
                   generator)                   codegen)

Linguagens         Todas         Todas         Todas         TypeScript
                                 (polyglot)                   only

Caching HTTP       Nativo        Não           Difícil       Não
                   (ETag, 304)   (POST only)   (POST)        (mas React
                                                              Query)

Tooling            Vasto         Bom           Ótimo         Crescendo
                   (Postman,     (grpcurl,     (Apollo       (tRPC
                   Swagger)      Buf)          Studio)       panel)

Learning Curve     Baixa         Alta          Média         Baixa
                                                              (se sabe TS)

Ideal Use Case     APIs          Microserviços Mobile com    Monorepo
                   públicas      internos,     queries       TypeScript
                                 polyglot,     variadas      full-stack
                                 streaming

6.2 Quando Usar Cada

REST — Escolha quando:

  • A API é pública e precisa ser consumida por clientes de qualquer linguagem
  • Caching HTTP é importante (CDN, browser cache, ETags)
  • A equipe prioriza simplicidade e convenção sobre performance
  • Operações CRUD mapeiam naturalmente para recursos e verbos HTTP

gRPC — Escolha quando:

  • Comunicação entre microserviços internos (não expostos ao browser)
  • Ambiente polyglot (Go, Java, Python, etc.) precisa de contrato forte
  • Streaming bidirecional é requisito (chat, telemetria, IoT)
  • Performance é crítica — latência p99 e throughput importam
  • A equipe aceita o overhead de protobuf tooling e code generation

GraphQL — Escolha quando:

  • Múltiplos consumers (web, mobile, third-party) com necessidades de dados diferentes
  • O grafo de dados é complexo com muitas relações
  • Over-fetching/under-fetching é um problema real (especialmente mobile)
  • A equipe aceita a complexidade de resolvers, DataLoader e N+1

tRPC — Escolha quando:

  • Monorepo TypeScript (Next.js, Remix, etc.) onde client e server estão juntos
  • Developer experience é prioridade máxima (autocomplete, refactoring)
  • Equipe é 100% TypeScript e não precisa de interoperabilidade com outras linguagens
  • API é interna — não precisa ser consumida por clientes externos

6.3 Padrão Híbrido: gRPC Interno + REST/GraphQL Externo

Na prática, a maioria dos sistemas de escala usa uma combinação:

                    Internet


              ┌─────────────────┐
              │   API Gateway   │
              │  (REST/GraphQL) │
              │                 │
              │  Rate limiting  │
              │  Auth           │
              │  Transformação  │
              │  proto ↔ JSON   │
              └────────┬────────┘
                       │ gRPC
          ┌────────────┼────────────┐
          │            │            │
          ▼            ▼            ▼
   ┌────────────┐ ┌─────────┐ ┌─────────────┐
   │  Order     │ │ Product │ │  Payment    │
   │  Service   │ │ Service │ │  Service    │
   │  (Go)      │ │ (Go)    │ │  (Java)     │
   └──────┬─────┘ └────┬────┘ └──────┬──────┘
          │ gRPC       │ gRPC        │ gRPC
          ▼            ▼             ▼
   ┌────────────┐ ┌─────────┐ ┌─────────────┐
   │ Inventory  │ │ Search  │ │  Fraud      │
   │ Service    │ │ Service │ │  Detection  │
   └────────────┘ └─────────┘ └─────────────┘

O API Gateway traduz REST/GraphQL externo para gRPC interno. Isto dá o melhor dos dois mundos: APIs públicas com boa DX para consumidores externos, e comunicação interna de alta performance com type safety forte.


7. gRPC-Web e Connect

7.1 gRPC-Web

Browsers não expõem controlo sobre HTTP/2 frames — não é possível usar gRPC nativo. gRPC-Web é um protocolo adaptado que funciona sobre HTTP/1.1 ou HTTP/2, mas requer um proxy (tipicamente Envoy) para traduzir entre gRPC-Web e gRPC nativo.

Browser                Envoy Proxy            gRPC Server
   │                      │                       │
   │── gRPC-Web ─────────▶│                       │
   │   (HTTP/1.1 ou       │── gRPC nativo ───────▶│
   │    HTTP/2, mas       │   (HTTP/2 com         │
   │    sem trailers      │    trailers)           │
   │    nativos)          │                       │
   │                      │◀── gRPC response ─────│
   │◀── gRPC-Web resp ───│                       │
   │   (trailers no body) │                       │

Limitações: apenas unary e server-streaming. Client-streaming e bidirectional não são suportados.

7.2 Connect (Buf)

Connect é uma alternativa moderna ao gRPC-Web criada pela Buf. Gera serviços que falam três protocolos simultaneamente:

┌────────────────────────────────────────────────────┐
│              Connect Server                        │
│                                                    │
│  Protocolo 1: Connect (nativo)                     │
│    → HTTP/1.1 ou HTTP/2                            │
│    → JSON ou Protobuf                              │
│    → Funciona direto no browser sem proxy          │
│    → Streaming via Server-Sent Events              │
│                                                    │
│  Protocolo 2: gRPC                                 │
│    → Compatível com qualquer client gRPC           │
│    → HTTP/2 obrigatório                            │
│                                                    │
│  Protocolo 3: gRPC-Web                             │
│    → Compatível com grpc-web clients               │
│    → Sem necessidade de Envoy                      │
│                                                    │
└────────────────────────────────────────────────────┘

Isto elimina a necessidade de proxy para comunicação browser-to-server. O client Connect pode chamar o server via fetch API normal:

import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { ProductService } from "./gen/ecommerce/v1/product_connect";

const transport = createConnectTransport({
  baseUrl: "https://api.example.com",
});

const client = createClient(ProductService, transport);

// Unary — retorna Promise
const product = await client.getProduct({ id: "prod-123" });

// Server streaming — async iterator
for await (const update of client.trackOrder({ orderId: "order-456" })) {
  console.log(`Status: ${update.status}, Mensagem: ${update.message}`);
}

7.3 Buf CLI

Buf substitui protoc com melhor DX, linting e breaking change detection:

# buf.yaml — configuração do módulo
version: v2
modules:
  - path: proto
lint:
  use:
    - STANDARD
  except:
    - FIELD_LOWER_SNAKE_CASE
breaking:
  use:
    - FILE

# Lint — detectar problemas no .proto
buf lint proto/
# error: ecommerce/v1/order.proto:15:3 - Enum value name
# "PENDING" should be prefixed with "ORDER_STATUS_"

# Breaking change detection contra a branch main
buf breaking proto/ --against '.git#branch=main'
# error: ecommerce/v1/product.proto:8:3 - Field "4" on message
# "Product" changed type from "int32" to "string"

# Gerar código (substitui protoc)
buf generate
# buf.gen.yaml
version: v2
plugins:
  - remote: buf.build/protocolbuffers/go
    out: gen/go
    opt: paths=source_relative
  - remote: buf.build/connectrpc/go
    out: gen/go
    opt: paths=source_relative
  - remote: buf.build/connectrpc/es
    out: gen/ts
    opt: target=ts

8. Exercicios

Exercicio 1 — Protobuf Schema Design

Projete um arquivo .proto completo para um sistema de reservas de hotel. Deve incluir:

  • Message Hotel com campos para nome, localização (lat/lng), estrelas, amenities (repeated), e metadata (map)
  • Message Room com oneof para tipos (Standard, Suite, Presidential) cada um com campos específicos
  • Message Reservation com status enum, datas, guest info
  • Service ReservationService com: CreateReservation (unary), SearchRooms (server-streaming, para resultados incrementais), CheckAvailability (unary)
  • Use reserved fields para simular evolução de schema (campos removidos)

Critério: compile sem erros com buf lint e buf build.

Exercicio 2 — gRPC Server + Client em Go

Implemente o ReservationService do exercício anterior em Go:

  • Server com os 3 métodos, incluindo o server-streaming de SearchRooms
  • Unary interceptor de logging que registra method, duration e status code
  • Stream interceptor de auth que valida um token no metadata
  • Health check endpoint funcionando
  • Teste com grpcurl

Critério: grpcurl -plaintext localhost:50051 list deve listar os serviços. Stream de SearchRooms deve enviar resultados incrementalmente.

Exercicio 3 — tRPC CRUD Completo

Crie uma API tRPC com Next.js App Router para gerenciar uma lista de tarefas:

  • Router com procedures: list (query com cursor pagination), getById (query), create (mutation), update (mutation), delete (mutation)
  • Input validation com Zod para todos os inputs
  • Middleware de auth e de logging
  • Error handling com TRPCError
  • Frontend com React Query: useInfiniteQuery para a lista, useMutation com optimistic updates para create/delete
  • Usar Prisma como ORM

Critério: type safety completo — mudar um campo no router deve causar erro de tipo no frontend sem precisar de build.

Exercicio 4 — Benchmark gRPC vs REST

Crie dois serviços identicos (mesmo endpoint, mesma lógica) — um REST (Express/Fastify) e um gRPC — e compare:

  • Latência p50, p95, p99 com ghz (gRPC) e wrk/autocannon (REST)
  • Throughput (requests/segundo)
  • Tamanho do payload no wire (use Wireshark ou tcpdump)
  • CPU e memória do server sob carga

Critério: documente os resultados com gráficos. Explique as razões das diferenças observadas.

Exercicio 5 — API Gateway Pattern

Implemente o padrão híbrido: API Gateway REST que traduz para gRPC interno:

  • 2 microserviços gRPC em Go (Product e Order)
  • API Gateway em Go ou TypeScript que expõe REST para o exterior e chama os serviços via gRPC
  • O gateway deve propagar deadlines, request IDs (via metadata), e tratar erros gRPC mapeando para HTTP status codes
  • Load balancing client-side com round_robin entre múltiplas instâncias de cada serviço
  • Docker Compose para orquestrar tudo

Critério: curl no gateway deve funcionar. Kill de uma instância de serviço deve resultar em failover automático.


9. Referências