← Voltar para o blog

ConnectRPC em Go: API Protobuf sem a Complexidade do gRPC

Aprenda ConnectRPC em Go com Protobuf, Buf e net/http: gere cliente e servidor tipados, teste com curl e entenda quando usar Connect, gRPC ou REST.

Resposta rápida: use ConnectRPC em Go quando você quer uma API tipada com Protobuf, geração de clientes e compatibilidade com gRPC, mas prefere operar sobre o net/http da biblioteca padrão e manter chamadas unary fáceis de testar com HTTP e JSON. O servidor pode atender o protocolo Connect e, quando necessário, clientes gRPC e gRPC-Web. Para uma API pública orientada a recursos, REST ainda pode ser mais simples; para comunicação interna entre serviços e SDKs controlados pelo time, ConnectRPC é uma opção forte.

ConnectRPC não é “gRPC sem Protobuf” nem apenas outro framework HTTP. O contrato continua definido em arquivos .proto, e o código de cliente e servidor é gerado. A diferença prática está na integração direta com HTTP, na interoperabilidade entre protocolos e em uma experiência de desenvolvimento que reduz parte do atrito comum de gRPC.

Neste guia, você vai criar uma API de catálogo com Go, ConnectRPC e Buf, chamá-la com curl, escrever um cliente tipado e entender os trade-offs entre Connect, gRPC e REST.

ConnectRPC, gRPC ou REST: comparação rápida

CritérioConnectRPCgRPC tradicionalREST com JSON
ContratoProtobufProtobufOpenAPI opcional
Transporte comumHTTP/1.1 ou HTTP/2HTTP/2HTTP/1.1 ou HTTP/2
Cliente tipado geradoSimSimDepende do OpenAPI
Chamada unary com curl e JSONSimExige ferramenta ou gatewaySim
StreamingSuportado, com diferenças por protocoloForte suporteNormalmente SSE, WebSocket ou solução própria
NavegadorConnect e gRPC-WebPrecisa de gRPC-Web/proxyNativo
Integração Gonet/httpRuntime próprio de gRPCnet/http ou framework
Melhor encaixeAPIs internas e SDKs controladosEcossistema gRPC consolidadoAPIs públicas e recursos HTTP

A tabela não define um vencedor universal. Se a empresa já opera service mesh, interceptors, observabilidade e clientes gRPC em várias linguagens, trocar a implementação apenas por novidade gera pouco valor. Se um projeto novo quer Protobuf sem abrir mão de ferramentas HTTP familiares, ConnectRPC merece um protótipo.

Para revisar a alternativa consolidada, leia gRPC com Go e Protobuf. Quando também for necessário publicar uma interface REST, compare com gRPC-Gateway em Go.

Como o ConnectRPC funciona

Você descreve serviços e mensagens em Protobuf. Plugins geram:

  • tipos Go para as mensagens;
  • uma interface que o servidor implementa;
  • um handler HTTP para registrar no mux;
  • um cliente tipado para chamar o serviço.

O fluxo é este:

catalog.proto
    ↓ geração
catalog.pb.go          tipos Protobuf
catalog.connect.go     cliente, interface e handler Connect
implementação Go → net/http → clientes Connect, gRPC ou gRPC-Web

A regra arquitetural importante é separar o contrato gerado da regra de negócio escrita à mão. Você pode regenerar os arquivos sem apagar sua implementação, e mudanças incompatíveis ficam visíveis na revisão do .proto.

Preparando o projeto

Crie o módulo:

mkdir catalog-api
cd catalog-api
go mod init exemplo.com/catalog-api

go get connectrpc.com/connect
go get google.golang.org/protobuf

Instale a CLI do Buf e os plugins de geração conforme a documentação oficial do projeto. Uma forma comum no ambiente Go é:

go install github.com/bufbuild/buf/cmd/buf@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest

Confirme que o diretório de binários do Go está no PATH:

export PATH="$(go env GOPATH)/bin:$PATH"
buf --version
protoc-gen-go --version
protoc-gen-connect-go --version

Em CI, fixe versões em vez de depender permanentemente de @latest. O objetivo é fazer a mesma revisão gerar o mesmo código em todas as máquinas.

Definindo o serviço Protobuf

Crie proto/catalog/v1/catalog.proto:

syntax = "proto3";

package catalog.v1;

option go_package = "exemplo.com/catalog-api/gen/catalog/v1;catalogv1";

service CatalogService {
  rpc GetProduct(GetProductRequest) returns (GetProductResponse) {}
}

message GetProductRequest {
  string id = 1;
}

message GetProductResponse {
  Product product = 1;
}

message Product {
  string id = 1;
  string name = 2;
  int64 price_cents = 3;
}

O nome catalog.v1 cria espaço para evolução do contrato. O campo go_package define o import usado pelo código Go gerado. Em um repositório real, substitua exemplo.com/catalog-api pelo caminho do seu módulo.

Não reutilize números de campos removidos. Em Protobuf, o número faz parte do formato serializado. Ao excluir um campo publicado, marque seu nome e número como reserved para impedir reutilização acidental:

message Product {
  reserved 4;
  reserved "legacy_sku";

  string id = 1;
  string name = 2;
  int64 price_cents = 3;
}

Configurando o Buf

Na raiz, crie buf.yaml:

version: v2
modules:
  - path: proto
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

Depois crie buf.gen.yaml:

version: v2
plugins:
  - local: protoc-gen-go
    out: gen
    opt: paths=source_relative
  - local: protoc-gen-connect-go
    out: gen
    opt: paths=source_relative

Gere os arquivos:

buf lint
buf generate

O projeto passa a ter uma estrutura semelhante a esta:

catalog-api/
├── cmd/server/main.go
├── gen/catalog/v1/catalog.pb.go
├── gen/catalog/v1/catalogv1connect/catalog.connect.go
├── internal/catalog/service.go
├── proto/catalog/v1/catalog.proto
├── buf.gen.yaml
├── buf.yaml
└── go.mod

Os caminhos exatos seguem o pacote e as opções configuradas. Não edite os arquivos em gen/ manualmente: altere o .proto ou a configuração e gere novamente.

Implementando o servidor ConnectRPC

Crie internal/catalog/service.go:

package catalog

import (
    "context"
    "errors"

    "connectrpc.com/connect"
    catalogv1 "exemplo.com/catalog-api/gen/catalog/v1"
)

type Service struct{}

func (s *Service) GetProduct(
    ctx context.Context,
    req *connect.Request[catalogv1.GetProductRequest],
) (*connect.Response[catalogv1.GetProductResponse], error) {
    if req.Msg.GetId() == "" {
        return nil, connect.NewError(
            connect.CodeInvalidArgument,
            errors.New("id do produto é obrigatório"),
        )
    }

    if req.Msg.GetId() != "go-101" {
        return nil, connect.NewError(
            connect.CodeNotFound,
            errors.New("produto não encontrado"),
        )
    }

    response := connect.NewResponse(&catalogv1.GetProductResponse{
        Product: &catalogv1.Product{
            Id:         "go-101",
            Name:       "Go para Back-end",
            PriceCents: 12990,
        },
    })
    response.Header().Set("Catalog-Version", "2026-09")

    return response, nil
}

Em produção, não devolva erros sem contexto interno para os logs. O cliente deve receber um código estável e uma mensagem segura; detalhes de banco, stack trace e identificadores sensíveis ficam na observabilidade do servidor.

Você também pode usar connect.NewError com um erro descritivo seguro:

return nil, connect.NewError(
    connect.CodeInvalidArgument,
    fmt.Errorf("id do produto é obrigatório"),
)

Os códigos Connect correspondem ao modelo conhecido de gRPC, como invalid_argument, not_found, already_exists, permission_denied, unavailable e internal. Não devolva internal para todo caso: o código faz parte do contrato operacional de retry e tratamento no cliente.

Publicando com net/http

Crie cmd/server/main.go:

package main

import (
    "log"
    "net/http"
    "time"

    catalogv1connect "exemplo.com/catalog-api/gen/catalog/v1/catalogv1connect"
    "exemplo.com/catalog-api/internal/catalog"
)

func main() {
    mux := http.NewServeMux()

    path, handler := catalogv1connect.NewCatalogServiceHandler(
        &catalog.Service{},
    )
    mux.Handle(path, handler)

    server := &http.Server{
        Addr:              ":8080",
        Handler:           mux,
        ReadHeaderTimeout: 5 * time.Second,
        IdleTimeout:       60 * time.Second,
    }

    log.Printf("API ConnectRPC ouvindo em %s", server.Addr)
    log.Fatal(server.ListenAndServe())
}

Execute:

go run ./cmd/server

O handler gerado informa o path correto do serviço e traduz requisições HTTP para a interface implementada. Como ele é um http.Handler, middlewares compatíveis com net/http podem envolver o serviço para logging, autenticação, métricas e recuperação de panic.

Antes de publicar, adicione timeouts, graceful shutdown e health checks. O checklist de API Go em produção reúne essas verificações, e o guia de middleware em Go ajuda a evitar uma pilha de wrappers sem ordem definida.

Testando com curl

Uma vantagem do protocolo Connect para métodos unary é a chamada direta com JSON. Com o servidor local, execute:

curl \
  --header 'Content-Type: application/json' \
  --data '{"id":"go-101"}' \
  http://localhost:8080/catalog.v1.CatalogService/GetProduct

Resposta esperada:

{
  "product": {
    "id": "go-101",
    "name": "Go para Back-end",
    "priceCents": "12990"
  }
}

O int64 pode aparecer como string no mapeamento JSON de Protobuf para preservar precisão em ambientes como JavaScript. Não altere o contrato apenas para deixar uma demonstração mais bonita; valide como os clientes reais serializam e desserializam esses valores.

Para um ID inexistente, o servidor deve devolver um erro de protocolo com código not_found. Testar com curl é excelente para diagnóstico, mas não substitui testes automatizados nem o cliente gerado.

Criando um cliente Go tipado

Um cliente Go usa o mesmo contrato:

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"

    "connectrpc.com/connect"
    catalogv1 "exemplo.com/catalog-api/gen/catalog/v1"
    catalogv1connect "exemplo.com/catalog-api/gen/catalog/v1/catalogv1connect"
)

func main() {
    httpClient := &http.Client{
        Timeout: 3 * time.Second,
    }

    client := catalogv1connect.NewCatalogServiceClient(
        httpClient,
        "http://localhost:8080",
    )

    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    response, err := client.GetProduct(
        ctx,
        connect.NewRequest(&catalogv1.GetProductRequest{Id: "go-101"}),
    )
    if err != nil {
        fmt.Printf("erro: %v\n", err)
        return
    }

    fmt.Printf("%s: R$ %.2f\n",
        response.Msg.Product.Name,
        float64(response.Msg.Product.PriceCents)/100,
    )
}

O cliente recebe um http.Client, então você controla timeout, proxy, TLS e transporte. Reutilize a instância para aproveitar conexões; não crie um cliente novo a cada chamada.

Propague context.Context com um prazo coerente com o orçamento total da requisição. O guia de context, timeout e cancelamento em Go mostra como evitar operações que continuam rodando depois que o cliente desistiu.

Interceptors para autenticação e observabilidade

Interceptors cumprem um papel semelhante ao middleware: envolvem chamadas para aplicar comportamento transversal. Casos comuns incluem:

  • validar token ou identidade mTLS;
  • adicionar request ID;
  • registrar duração e código do procedimento;
  • criar spans do OpenTelemetry;
  • coletar métricas por serviço e método;
  • aplicar política de retry no cliente.

Evite colocar regra de autorização inteira em um interceptor global. Autenticação pode ser transversal, mas autorização por recurso normalmente depende do objeto carregado e deve permanecer próxima do caso de uso.

Também cuide da cardinalidade. Uma métrica com product_id, user_id ou mensagem de erro como label pode explodir a quantidade de séries. Prefira serviço, procedimento e código de status. IDs específicos ficam em logs ou traces, conforme sua política de privacidade.

Para instrumentação distribuída, consulte OpenTelemetry em Go e logging estruturado com slog.

Compatibilidade e evolução do contrato

Protobuf facilita evolução, mas não torna toda mudança segura. Boas práticas:

  1. adicione campos novos com números inéditos;
  2. não altere o significado de um campo existente;
  3. não troque tipos sem entender a compatibilidade wire e semântica;
  4. reserve nomes e números removidos;
  5. mantenha servidor e clientes compatíveis durante o rollout;
  6. execute verificação de breaking changes no CI;
  7. versione o pacote quando houver uma mudança realmente incompatível.

Com Buf, o CI pode comparar a branch com a referência principal:

buf lint
buf breaking --against '.git#branch=main'
buf generate

git diff --exit-code

O último comando detecta quando alguém alterou o .proto, mas esqueceu de commitar o código gerado. Algumas equipes preferem gerar apenas no build; outras versionam os arquivos para facilitar consumo e revisão. Escolha uma política e automatize-a.

Streaming exige atenção ao protocolo e ao proxy

ConnectRPC suporta streaming, mas a compatibilidade varia conforme protocolo, transporte, navegador e infraestrutura. Antes de desenhar um serviço de eventos, valide:

  • se o cliente usa Connect, gRPC ou gRPC-Web;
  • se a chamada precisa ser client streaming, server streaming ou bidirecional;
  • se o proxy mantém HTTP/2 e não aplica buffering inadequado;
  • quais timeouts existem no load balancer;
  • como cancelamento e backpressure são propagados;
  • como o fluxo será observado e limitado.

Para notificações simples ao navegador, WebSocket ou Server-Sent Events pode ser mais fácil de operar. Para comunicação interna já padronizada em gRPC, streaming RPC pode ser natural. Não escolha o protocolo apenas pelo exemplo mais curto.

Segurança antes do deploy

Uma API tipada ainda precisa das mesmas proteções de qualquer serviço:

  • TLS entre cliente e servidor;
  • autenticação e autorização explícitas;
  • limites de tamanho de mensagem;
  • deadlines no cliente e no servidor;
  • rate limiting nos procedimentos caros;
  • erros públicos sem dados internos;
  • validação semântica além do tipo Protobuf;
  • proteção contra overload e filas ilimitadas;
  • atualização e verificação de dependências.

Protobuf garante que price_cents seja um inteiro; ele não garante que o preço seja positivo, que o usuário possa acessar o produto ou que um ID pertença ao tenant autenticado.

Se o serviço estiver atrás de CORS para uso no navegador, configure origens e headers de forma restrita. gRPC-Web e Connect no browser não removem o modelo de segurança do navegador.

Quando ConnectRPC é a escolha certa

Considere ConnectRPC quando:

  • serviços internos compartilham contratos Protobuf;
  • você controla clientes e servidor;
  • SDKs gerados reduzem duplicação e erros;
  • existe necessidade de interoperar com gRPC;
  • o time quer usar net/http, middlewares e infraestrutura HTTP conhecida;
  • chamadas unary com JSON facilitam debugging;
  • evolução de schema e breaking changes precisam de governança.

Prefira REST quando:

  • a API será consumida por parceiros sem toolchain compartilhado;
  • URLs, métodos e recursos HTTP são parte importante do produto;
  • cache HTTP e exploração manual têm alto valor;
  • OpenAPI já é o padrão da organização;
  • geração obrigatória de cliente criaria mais atrito que segurança.

Prefira gRPC tradicional quando:

  • a plataforma já está consolidada em gRPC;
  • bibliotecas, proxies e observabilidade existentes dependem da implementação atual;
  • streaming avançado é central e já foi validado na infraestrutura;
  • mudar não resolve um problema operacional concreto.

Uma arquitetura pode combinar abordagens: ConnectRPC para comunicação interna tipada, REST para integrações públicas e eventos para processamento assíncrono. O risco aparece quando cada equipe inventa um protocolo diferente sem padrões de autenticação, erro, observabilidade e versionamento.

Checklist de produção para ConnectRPC em Go

Antes do deploy, confirme:

  • arquivos .proto passam por lint e breaking check;
  • geração é reproduzível e versões dos plugins estão fixadas;
  • cliente e servidor têm deadlines;
  • servidor implementa graceful shutdown;
  • autenticação e autorização foram testadas por procedimento;
  • limites de mensagem e concorrência estão definidos;
  • erros Connect usam códigos coerentes;
  • logs não armazenam tokens nem payloads sensíveis;
  • métricas evitam labels de alta cardinalidade;
  • traces propagam contexto entre serviços;
  • proxy e load balancer suportam o protocolo e streaming necessários;
  • health checks não dependem de todas as integrações externas;
  • rollout mantém compatibilidade com clientes antigos;

Conclusão

ConnectRPC em Go oferece um caminho pragmático para APIs RPC tipadas: contrato Protobuf, clientes gerados, compatibilidade com o ecossistema gRPC e integração com o servidor HTTP padrão. Sua melhor característica não é economizar algumas linhas, mas permitir que contrato, transporte e implementação evoluam com menos improvisação.

Comece com um método unary pequeno, gere um cliente Go e teste a chamada por HTTP. Depois valide autenticação, observabilidade, breaking changes e comportamento atrás do proxy real. Se essa experiência reduzir atrito sem comprometer requisitos de interoperabilidade, ConnectRPC pode ser uma base melhor que montar manualmente uma API interna ou adotar toda a complexidade de uma plataforma gRPC sem necessidade.

Próximos passos

Referências oficiais