grpc-gateway & REST Bridging

Why and how to expose a gRPC service as REST/JSON for browsers using the grpc-gateway pattern.

The problem: browsers can't easily speak gRPC

The introduction page in this track already covers why REST/JSON is usually still the right choice for a public, browser-facing API: browsers have no native way to make a raw gRPC call, gRPC's binary payloads aren't something you can casually inspect with curl or a browser's dev tools, and third-party API consumers generally benefit from JSON's simplicity and universal tooling. That leaves a common real-world situation unresolved: a team has standardized internally on gRPC between its own services, but still needs to expose some of that functionality to a browser-based frontend or external partners as ordinary REST/JSON — without hand-writing and maintaining a second, parallel REST API that just re-implements the same logic.

What grpc-gateway does

grpc-gateway is a protoc plugin that reads special HTTP-mapping annotations added directly to a .proto file, and generates a Go reverse-proxy server from them — one that accepts plain REST/JSON HTTP requests, translates each one into the equivalent gRPC call against your existing service, and translates the gRPC response back into JSON for the HTTP client. The actual service implementation is never touched or duplicated; the gateway is purely a generated translation layer sitting in front of it.

An annotated .proto file

The annotations come from Google's google.api.http extension, referenced from the .proto file:

Protobuf
syntax = "proto3";

package tasks.v1;

import "google/api/annotations.proto";

service TaskService {
  rpc GetTask (GetTaskRequest) returns (Task) {
    option (google.api.http) = {
      get: "/v1/tasks/{id}"
    };
  }

  rpc CreateTask (CreateTaskRequest) returns (Task) {
    option (google.api.http) = {
      post: "/v1/tasks"
      body: "*"
    };
  }
}

Each option (google.api.http) block maps one RPC method onto one REST-style route: GetTask becomes GET /v1/tasks/{id} (with {id} bound from the URL path into the request message's id field), and CreateTask becomes POST /v1/tasks with the entire JSON request body (body: "*") mapped onto the request message.

Generating the gateway

Bash
protoc -I . \
  --go_out=. --go-grpc_out=. \
  --grpc-gateway_out=. \
  tasks.proto

This produces an additional generated file exposing a registration function (RegisterTaskServiceHandlerFromEndpoint), wired up in a small, separate gateway binary that runs alongside the real gRPC server:

Go
func main() {
    ctx := context.Background()
    mux := runtime.NewServeMux()

    err := gwpb.RegisterTaskServiceHandlerFromEndpoint(
        ctx, mux, "localhost:9090",
        []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())},
    )
    if err != nil {
        log.Fatal(err)
    }

    log.Println("REST gateway listening on :8080, forwarding to gRPC on :9090")
    http.ListenAndServe(":8080", mux)
}

The resulting picture

Plaintext
Browser / REST client --REST/JSON--> grpc-gateway (:8080) --gRPC--> TaskService (:9090)
                                                                          ^
Internal gRPC clients ---------------------gRPC------------------------- |

Internal services keep calling TaskService directly over gRPC on :9090, getting every benefit covered earlier in this track — compact binary payloads, a strongly-typed generated contract, HTTP/2 multiplexing. A browser (or any plain HTTP client) instead talks to the gateway on :8080 with ordinary JSON over regular HTTP, with no awareness that gRPC is involved anywhere behind it. The .proto file remains the single source of truth for both — the REST shape is generated directly from the same annotations that define the gRPC service, so the two can never silently drift apart the way a hand-maintained parallel REST API could.

Common mistakes

  • Hand-writing REST endpoints that duplicate a gRPC service's logic, instead of generating the translation layer from google.api.http annotations on the same .proto file that already defines the service.
  • Forgetting that the gateway and the actual gRPC server are two separate running processes, listening on two separate ports — both need to be deployed, kept in sync on version, and monitored for health independently.
  • Assuming grpc-gateway can transparently expose every gRPC feature as REST — bidirectional and client-streaming RPCs don't map cleanly onto a single HTTP request/response, so gateway-exposed endpoints are realistically limited to unary calls and server-streaming (translated into a chunked HTTP response).

Interview questions

Q: What problem does grpc-gateway solve? It lets a gRPC service also be reachable as REST/JSON by clients that can't (or shouldn't have to) speak gRPC directly — browsers, third-party integrators, simple curl-based tooling — by generating a reverse-proxy server from google.api.http annotations already present in the .proto file, without touching or duplicating the actual service implementation.

Q: Where do the REST route mappings in a grpc-gateway setup actually come from? From option (google.api.http) = { ... } blocks added directly to each RPC method in the .proto file, specifying the HTTP verb, path (with path parameters bound to request message fields), and how the request body maps onto the message. protoc with the grpc-gateway plugin reads these to generate the translating proxy.

Q: Can every kind of gRPC call be exposed through grpc-gateway equally well? Not quite — unary calls map cleanly onto a normal HTTP request/response, and server streaming can be translated into a chunked HTTP response, but client streaming and bidirectional streaming don't have a natural equivalent in a single HTTP request/response exchange, so those RPC shapes are typically not exposed through the gateway at all.