Skip to content

Day 123 — grpc-gateway / REST Transcoding

Month 5 · Week 2 · ⬅ Day 122 · Day 124 ➡ · Journal index

🎯 Learning Objective

Expose a gRPC service as a RESTful JSON API using grpc-gateway, and understand how HTTP requests are transcoded into RPC calls.

📚 Topics

  • google.api.http annotations and path templates ({id})
  • protoc-gen-grpc-gateway reverse proxy + runtime.ServeMux
  • HTTP method/path → RPC mapping; JSON ↔ protobuf

📖 Reading / Sources

📝 Notes

  • grpc-gateway generates a reverse proxy: an HTTP/JSON request comes in, it transcodes it into a gRPC call to your backend, then transcodes the response back to JSON. One service definition serves both gRPC and REST clients → [[grpc-gateway]] [[transcoding]].
  • You annotate RPCs in the .proto with google.api.http: option (google.api.http) = { get: "/v1/users/{id}" };. The {id} path template binds a URL segment to a request field; body: "*" maps the JSON body to the request message for POST/PUT → [[path-template]].
  • The generated RegisterXxxHandlerFromEndpoint wires a runtime.ServeMux to a backend address; you serve it with an ordinary http.Server. Often the gateway and gRPC server run in the same binary, the gateway dialing localhost → [[reverse-proxy]].
  • Errors transcode too: a gRPC codes.NotFound becomes HTTP 404 via the gateway's code→status table (the same mapping I rebuilt on Day 118) → [[status-codes]].
  • The matcher tries routes in order, compares method + segment count, and extracts {name} segments into request fields. Literal segments must match exactly; a miss is a 404 → [[routing]].
  • grpc-gateway can also emit an OpenAPI/Swagger spec (protoc-gen-openapiv2) from the same annotations, so REST consumers get docs for free.
  • Trade-off: the gateway adds a marshal/unmarshal hop and loses streaming semantics for unary-style REST. Use it for browser/3rd-party REST clients; keep gRPC for service-to-service.

💻 Code Examples

// user.proto — the annotation that drives transcoding
service Users {
  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: "*" };
  }
}
// main.go — run the gateway in front of the gRPC backend
mux := runtime.NewServeMux()
err := userpb.RegisterUsersHandlerFromEndpoint(ctx, mux, "localhost:50051",
    []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())})
http.ListenAndServe(":8080", mux) // GET /v1/users/42 -> GetUser{id:"42"}

Stdlib rebuild of the matcher (/v1/users/{id} → params → JSON, no protoc): examples/month-05/transcoding/main.go · Run: go run ./examples/month-05/transcoding

🏋️ Exercises / Practice

Exercise Status Link
Parse/Match grpc-gateway path templates exercises/month-05/week-2/pathtemplate
Extract {id}/{book_id} into a params map exercises/month-05/week-2/pathtemplate

🐛 Mistakes Made

  • Forgot body: "*" on the POST and the request body never populated the message — the gateway only maps the body when told to.
  • Assumed {id} could span multiple path segments; by default it matches a single segment (you need {id=**} for greedy matching).

❓ Open Questions

  • How are query parameters mapped to nested request fields, and how does ?field.subfield= notation work?

🧠 Active Recall (answer without looking)

  1. Q: In get: "/v1/users/{id}", what does {id} do?
AIt's a path-template binding: the matching URL segment is extracted and assigned to the request message's `id` field before the RPC is called. By default it matches exactly one segment.
  1. Q: A gRPC backend returns codes.NotFound. What HTTP status does the gateway send?
A404 — grpc-gateway maps gRPC status codes to HTTP status codes (NotFound→404, InvalidArgument→400, Unauthenticated→401, etc.).

🪶 Feynman Reflection

grpc-gateway is a translator standing between REST clients and a gRPC server. A browser says "GET /v1/users/42"; the translator reads the path template, pulls "42" into a GetUserRequest{id:"42"}, makes the gRPC call, takes the protobuf reply, and reads it back out as JSON. One .proto, two front doors.

🕳️ Knowledge Gaps

  • Query-parameter → field mapping rules, and custom error/marshaler hooks on ServeMux.

✅ Summary

I understand how google.api.http annotations transcode HTTP+JSON into gRPC calls, how path templates bind URL segments to fields, and the unary-vs-streaming trade-off of fronting gRPC with REST.

⏭️ Next Steps / Prep for Tomorrow

  • Day 124: server reflection and poking services with grpcurl.

Time spent Difficulty Confidence
90 min 🟦🟦🟦⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: feat(examples): grpc-gateway REST transcoding matcher (day 123)