gRPC-Gateway & Transcoding
Two audiences, one contract
Section titled “Two audiences, one contract”In the foundations module we said the same system often wants gRPC internally and REST at the edge — fast, typed calls between your own services, but plain JSON for third parties, browsers, and quick curl checks.
Transcoding lets you serve both from the same .proto. You annotate each method with the REST route it should also answer, and a generated reverse proxy translates incoming JSON/HTTP requests into gRPC calls and the protobuf responses back into JSON.
Annotate the methods
Section titled “Annotate the methods”You describe the REST mapping right in the service definition, using google.api.http annotations:
import "google/api/annotations.proto";
service UserService { rpc GetUser(GetUserRequest) returns (User) { option (google.api.http) = { get: "/v1/users/{id}" }; }
rpc CreateUser(CreateUserRequest) returns (User) { option (google.api.http) = { post: "/v1/users" body: "*" }; }}GetUser now also answers GET /v1/users/42 — the {id} path segment maps onto the request’s id field. CreateUser answers POST /v1/users with the JSON body mapped onto the request message. The gRPC method is unchanged; the annotation just adds a REST door.
One .proto, two front doors
Section titled “One .proto, two front doors”flowchart TB proto[".proto + google.api.http annotations"] --> svc["UserService (gRPC impl)"] internal["internal services (native gRPC)"] --> svc ext["external client GET /v1/users/42"] --> gw["grpc-gateway reverse proxy"] gw -->|translates JSON↔protobuf| svc
The gateway is generated code (grpc-gateway in the Go ecosystem; Envoy also does transcoding via a filter). It parses the URL and JSON, builds the protobuf request, calls the real gRPC method, and marshals the reply back to JSON — all driven by the annotations.
// Run the gateway alongside the gRPC servermux := runtime.NewServeMux()err := userv1.RegisterUserServiceHandlerFromEndpoint( ctx, mux, "localhost:50051", []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())},)if err != nil { log.Fatal(err) }http.ListenAndServe(":8080", mux) // REST/JSON on :8080, gRPC on :50051# Python typically uses Envoy's grpc_json_transcoder filter,# pointed at the same descriptor set the .proto compiles to:# typed_config:# proto_descriptor: "user_descriptor.pb"# services: ["user.v1.UserService"]// Node commonly fronts gRPC with Envoy transcoding as well,// or uses Connect, which can serve JSON directly from the same service.// Either way the mapping lives in the .proto annotations, not hand-written routes.When to reach for it
Section titled “When to reach for it”- You have external or browser consumers who expect REST/JSON but you don’t want to hand-maintain a second API. Transcoding keeps one contract.
- You want
curl-ability and an OpenAPI doc. The same annotations can generate an OpenAPI spec. - Skip it when everything is internal. If only your own services call the API, the extra proxy hop and JSON conversion are pure overhead — stay on native gRPC.