Skip to content

gRPC-Gateway & Transcoding

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.

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.

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 same service, reached as gRPC or as REST/JSON

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 server
mux := 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
  • 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.
What does transcoding let you do?
How is the REST route for a method declared?
What does the generated gateway actually do at runtime?
When is transcoding NOT worth it?