A service declares rpc methods: a name, a request message, and a response message for each. Protobuf only defines the interface. An RPC framework such as gRPC or ConnectRPC turns the declaration into clients and servers, picks the transport, and handles the networking.
A method is unary when one request gets one response. Marking either side with stream lets that side send a sequence of messages within a single call: a server stream for a feed of results, a client stream for an upload, or both at once for a chat. The example declares one method of each kind.
In Go, the message plugin covered in Generating Code only produces message and enum types. Clients and servers come from an additional framework-specific plugin: protoc-gen-go-grpc for gRPC, and protoc-gen-connect-go for ConnectRPC. In TypeScript, protoc-gen-es also generates the service definition that Connect-ES builds clients from.
service UserService { // Unary: One request, one response rpc GetUser(GetUserRequest) returns (User); // Server Stream: One request, many responses rpc ListUsers(ListUsersRequest) returns (stream User); // Client Stream: Many requests, one response rpc UploadAvatar(stream AvatarChunk) returns (UploadAvatarResponse); // Bidirectional Stream: Real-time chat rpc Chat(stream Message) returns (stream Message); }
This guide covers two RPC frameworks: gRPC, in this section, and ConnectRPC, in the next section. Both turn a service definition into a typed client and server, and both send Protobuf messages over HTTP. They differ in the HTTP versions they need, which clients can call them, and how much comes built in.
Strengths
- The most widely deployed Protobuf RPC framework, with official implementations for nearly every major language.
- Service meshes, proxies like Envoy, and cloud load balancers understand it natively.
- Deadlines, health checks, server reflection, and client-side load balancing come built in.
Trade-offs
- Requires HTTP/2 end to end, including trailers. Some L7 proxies and load balancers don't pass HTTP/2 trailers through.
- Browsers can't make gRPC calls directly, so web apps need gRPC-Web and a translating proxy in front of the server.
- The wire format is binary framing, so you debug with tools like grpcurl rather than curl or browser devtools.
More details and examples at grpc.io.
The example service
The examples in this section and the next use this schema. One buf generate produces everything: message types from protoc-gen-go, the gRPC server and client from protoc-gen-go-grpc, the ConnectRPC handler and client from protoc-gen-connect-go, and the TypeScript messages and service definition from protoc-gen-es.
edition = "2024"; package user.v1; service UserService { rpc GetUser(GetUserRequest) returns (GetUserResponse); } message GetUserRequest { int64 id = 1; } message GetUserResponse { User user = 1; } message User { int64 id = 1; string name = 2; }
version: v2 managed: enabled: true override: - file_option: go_package_prefix value: example.com/gen/go plugins: - remote: buf.build/protocolbuffers/go:v1.36.12 out: gen/go opt: paths=source_relative - remote: buf.build/grpc/go:v1.6.2 out: gen/go opt: paths=source_relative - remote: buf.build/connectrpc/go:v2.0.2 out: gen/go opt: paths=source_relative - remote: buf.build/bufbuild/es:v2.14.0 out: web/src/gen opt: target=ts
gRPC between microservices
A users service implements the generated interface and registers it with a gRPC server. Any other service in the cluster dials it and calls GetUser like a local function, with the request and response types checked by the compiler on both sides. Embedding UnimplementedUserServiceServer keeps the server compiling when new methods are added to the schema.
type userServer struct { userv1.UnimplementedUserServiceServer } func (s *userServer) GetUser( ctx context.Context, req *userv1.GetUserRequest, ) (*userv1.GetUserResponse, error) { return userv1.GetUserResponse_builder{ User: userv1.User_builder{ Id: proto.Int64(req.GetId()), Name: proto.String("Ada"), }.Build(), }.Build(), nil } func main() { listener, err := net.Listen("tcp", ":8080") if err != nil { log.Fatal(err) } server := grpc.NewServer() userv1.RegisterUserServiceServer(server, &userServer{}) log.Fatal(server.Serve(listener)) }
conn, err := grpc.NewClient( "users.internal:8080", grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { return err } defer conn.Close() users := userv1.NewUserServiceClient(conn) req := userv1.GetUserRequest_builder{Id: proto.Int64(42)}.Build() res, err := users.GetUser(ctx, req) if err != nil { return err } fmt.Println(res.GetUser().GetName())
ConnectRPC uses the same service definitions and generated message types as gRPC. Its servers answer three protocols on one port: its own Connect protocol, gRPC, and gRPC-Web.
Strengths
- The Connect protocol works over HTTP/1.1 or HTTP/2. A unary call is a plain POST with a JSON or binary body.
- Browsers can call a Connect server directly, with no proxy in between.
- A Connect server also speaks gRPC over HTTP/2 and gRPC-Web over either version, so existing gRPC clients keep working.
- Builds on each language's standard HTTP stack, such as net/http in Go, so existing middleware applies.
Trade-offs
- Supports fewer languages than gRPC.
- Less built-in infrastructure: features like xDS load balancing are left to the surrounding platform.
More details and examples at connectrpc.com.
A ConnectRPC server and client
With connect-go, the handler has the same shape as the gRPC one, but it is mounted on a standard net/http server. That means standard Go HTTP libraries and middleware work with ConnectRPC without changes. The server answers Connect, gRPC, and gRPC-Web on one port, so the gRPC client from the previous section can call it unchanged. The ConnectRPC client takes a standard http.Client too.
type userServer struct { userv1connect.UnimplementedUserServiceHandler } func (s *userServer) GetUser( ctx context.Context, req *userv1.GetUserRequest, ) (*userv1.GetUserResponse, error) { return userv1.GetUserResponse_builder{ User: userv1.User_builder{ Id: proto.Int64(req.GetId()), Name: proto.String("Ada"), }.Build(), }.Build(), nil } func main() { server := connect.NewServer() userv1connect.RegisterUserServiceHandler(server, &userServer{}) mux := http.NewServeMux() connecthttp.Mount(mux, server) protocols := new(http.Protocols) protocols.SetHTTP1(true) protocols.SetUnencryptedHTTP2(true) httpServer := &http.Server{ Addr: ":8080", Handler: mux, Protocols: protocols, } log.Fatal(httpServer.ListenAndServe()) }
client := connect.NewClient( connecthttp.NewTransport(http.DefaultClient, "http://users.internal:8080"), ) users := userv1connect.NewUserServiceClient(client) req := userv1.GetUserRequest_builder{Id: proto.Int64(42)}.Build() res, err := users.GetUser(ctx, req) if err != nil { return err } fmt.Println(res.GetUser().GetName())
ConnectRPC in the browser
With Connect-ES, you call the service from TypeScript in the browser. Here it calls the Go server above. The browser speaks the Connect protocol, which the server understands directly, so unlike gRPC+gRPC-Web there is no proxy in between. Each call shows up in devtools as an ordinary POST. Because a unary call is just HTTP with a JSON body, curl works too.
import { createClient } from "@connectrpc/connect"; import { createConnectTransport } from "@connectrpc/connect-web"; import { UserService } from "./gen/user/v1/user_pb"; const transport = createConnectTransport({ baseUrl: "https://api.example.com", }); const users = createClient(UserService, transport); const res = await users.getUser({ id: 42n }); console.log(res.user?.name);
$ curl https://api.example.com/user.v1.UserService/GetUser \
-H "Content-Type: application/json" \
-d '{"id": "42"}'
{"user":{"id":"42","name":"Ada"}}Further Reading
- gRPC
The original Protobuf RPC framework, with implementations for most major languages.
- ConnectRPC
A CNCF RPC framework built on Protobuf services, compatible with gRPC clients and servers. Its origin story is in Connect: A better gRPC.
Next
Advanced
Imports and editions, field presence, unknown fields, and which schema changes break clients that are already deployed.