One backend, two languages
You have a Go service handling user data. The mobile team wants gRPC for speed and strong typing. The web frontend team needs standard JSON over HTTP because their framework expects it. You don't want to maintain two separate codebases for the same business logic. You want one source of truth.
gRPC-Gateway solves this by acting as a translator. It sits in front of your gRPC server and converts incoming REST requests into gRPC calls. The mobile app talks gRPC directly. The web app talks HTTP/JSON to the gateway. The gateway forwards the request to your gRPC server, gets the response, translates it back to JSON, and sends it to the web app. Your Go code implements only the gRPC service. The gateway handles the REST surface.
Think of gRPC-Gateway as a bilingual waiter. The kitchen only speaks gRPC. It takes orders in a specific format and sends back dishes in a specific format. The web clients speak HTTP/JSON. They wave their hands and shout in JSON. The waiter intercepts the JSON shout, translates it into a gRPC order, hands it to the kitchen, gets the gRPC dish back, translates it into a JSON plate, and serves the web client. The kitchen never knows the web client exists. It just processes gRPC calls.
How the gateway maps requests
The magic lives in the .proto file. You annotate RPCs with google.api.http. This tells the code generator how to map the RPC to an HTTP method and path. The generator creates a RegisterGreeterHandlerFromEndpoint function. This function builds the reverse proxy. It reads the HTTP request, decodes the JSON, calls the gRPC method, encodes the response, and writes the HTTP response.
Here's the proto definition for a simple service. The option (google.api.http) block binds the RPC to a GET endpoint with a path parameter.
// api/greeter.proto
syntax = "proto3";
package api;
option go_package = "example.com/api";
import "google/api/annotations.proto";
// Greeter defines the service contract for greeting users.
service Greeter {
// SayHello maps to a GET request with a path parameter
rpc SayHello (HelloRequest) returns (HelloResponse) {
option (google.api.http) = {
get: "/v1/greeter/{name}"
};
// Path parameter {name} binds to the 'name' field in HelloRequest
}
}
message HelloRequest { string name = 1; }
message HelloResponse { string message = 1; }
Run the protoc command to generate the Go code, gRPC stubs, and gateway handlers. The paths=source_relative option keeps file paths relative to the proto file location, which matches Go module conventions.
# Generates Go code, gRPC server stubs, and gateway mux handlers
# paths=source_relative keeps file paths relative to the proto file location
protoc --go_out=. --go-grpc_out=. --grpc-gateway_out=. \
--grpc-gateway_opt logtostderr=true \
--grpc-gateway_opt paths=source_relative \
api/*.proto
The generator produces greeter.pb.go, greeter_grpc.pb.go, and greeter.pb.gw.go. The gateway file contains the RegisterGreeterHandlerFromEndpoint function. You call this function in your main.go to wire up the translation layer.
Minimal working example
Here's the server struct and the RPC implementation. The receiver name is s, one letter matching the type. This follows Go convention. The method signature takes context.Context as the first parameter, named ctx. Functions that take a context should respect cancellation and deadlines. The gateway injects HTTP headers into this context as metadata, so your handler can access them.
// server implements the Greeter service defined in the proto file.
type server struct {
api.UnimplementedGreeterServer
}
// SayHello returns a greeting message for the given name.
func (s *server) SayHello(ctx context.Context, in *api.HelloRequest) (*api.HelloResponse, error) {
// Context carries deadlines and cancellation from the client
// The gateway injects HTTP headers into this context as metadata
return &api.HelloResponse{Message: "Hello " + in.Name}, nil
}
The UnimplementedGreeterServer embed ensures forward compatibility. If you add a new RPC to the proto file, the compiler rejects the program with *server does not implement api.GreeterServer (missing method NewMethod) until you implement the new method. This prevents silent failures when the API evolves.
Now wire everything together. The gRPC server and the gateway mux share the same network listener. Go's net.Listener can be passed to multiple servers. The gRPC server runs in a goroutine. The HTTP server runs in the main goroutine. Both accept connections on port 8080. gRPC connections speak HTTP/2. REST connections speak HTTP/1.1. The listener handles both.
// runServer starts the gRPC server and gateway mux on a single port.
func runServer() {
lis, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
grpcSrv := grpc.NewServer()
api.RegisterGreeterServer(grpcSrv, &server{})
// Run gRPC in a goroutine so the HTTP server can start
go func() {
// Serve blocks until the server stops
_ = grpcSrv.Serve(lis)
}()
mux := runtime.NewServeMux()
// Register gateway handlers pointing to the local gRPC endpoint
err = api.RegisterGreeterHandlerFromEndpoint(context.Background(), mux, "localhost:8080", nil)
if err != nil {
log.Fatal(err)
}
// HTTP server serves REST requests and forwards them to gRPC
httpSrv := &http.Server{Handler: mux}
log.Fatal(httpSrv.Serve(lis))
}
func main() {
runServer()
}
The RegisterGreeterHandlerFromEndpoint call returns an error if it can't connect to the gRPC endpoint. You check it immediately. if err != nil { log.Fatal(err) }. The community accepts this boilerplate because it makes failure visible at startup. The gateway needs to dial the gRPC server. Even though they run on the same machine, the gateway uses a network connection. Passing nil for options uses default dial settings.
One listener, two protocols. The gateway handles the translation.
Realistic usage with context and errors
Real services need context propagation and error handling. The gateway automatically copies HTTP headers into the gRPC context. Your gRPC handler receives this context. You can extract auth tokens or request IDs. The gateway also maps gRPC status codes to HTTP status codes. codes.NotFound becomes 404. codes.InvalidArgument becomes 400.
Here's a proto for a user service with a path parameter.
// api/user.proto
syntax = "proto3";
package api;
option go_package = "example.com/api";
import "google/api/annotations.proto";
service UserService {
rpc GetUser (GetUserRequest) returns (User) {
option (google.api.http) = {
get: "/v1/users/{id}"
};
}
}
message GetUserRequest { string id = 1; }
message User { string id = 1; string email = 2; }
The handler extracts metadata and returns a gRPC error. The gateway translates the error to an HTTP response. You don't need to write custom error handling logic for standard cases.
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// GetUser retrieves user details by ID.
func (s *server) GetUser(ctx context.Context, req *api.GetUserRequest) (*api.User, error) {
// Gateway copies HTTP headers to context metadata
// Access them via metadata package functions
reqID := metadata.ValueFromIncomingContext(ctx, "x-request-id")
user, err := s.db.FindByID(ctx, req.Id)
if err != nil {
// Return gRPC error; gateway maps codes.NotFound to HTTP 404
return nil, status.Error(codes.NotFound, "user not found")
}
return user, nil
}
The metadata.ValueFromIncomingContext call retrieves the header value. The gateway sets this metadata before calling the RPC. If the header is missing, the slice is empty. Check the length before indexing. The status.Error function creates a gRPC status with a code and message. The gateway reads the code and sets the HTTP status. The message appears in the JSON response body.
Context flows through the gateway. Headers become metadata. Errors become status codes.
Pitfalls and runtime behavior
The gateway buffers the entire HTTP body before calling the gRPC method. This means the gRPC handler receives the full payload in memory. For large file uploads, this can spike memory usage. If you need streaming uploads, consider a different approach or use gRPC client streaming with a custom gateway configuration. The gateway supports server streaming out of the box. It converts the stream to chunked transfer encoding. Client streaming requires more setup.
If you forget the google.api.http annotation, the generator skips the HTTP mapping. You get a gRPC server but no REST endpoint. The compiler won't catch this; it's a code generation behavior. Check the generated .pb.gw.go file to verify handlers were created.
If you call RegisterGreeterHandlerFromEndpoint with a bad address, you get a runtime error like dial tcp localhost:9090: connect: connection refused. Ensure the gRPC server is listening before the gateway tries to dial it. In the minimal example, the gRPC server starts in a goroutine before the gateway registers. This ordering matters.
If you don't implement the interface, the compiler rejects the program with cannot use &server{} (type *server) as api.GreeterServer value in argument: *server does not implement api.GreeterServer (missing method SayHello). Embed UnimplementedGreeterServer to avoid this.
The gateway mux and gRPC server share the listener. If you stop the HTTP server, the gRPC server keeps running. If you stop the gRPC server, the HTTP server keeps running but requests fail. Manage both lifecycles explicitly in production. Use context.Context to coordinate shutdown.
The gateway buffers the body. Watch your memory usage on large uploads.
When to use gRPC-Gateway
Use gRPC-Gateway when you need to serve both REST clients and gRPC clients from a single Go backend without duplicating business logic.
Use a standard net/http handler when your API is simple, you don't need strong typing, or you prefer manual JSON marshaling for full control over the response format.
Use a separate REST service when the REST API has significantly different requirements, such as different rate limits, authentication flows, or response shapes that don't map cleanly to the gRPC model.
Use raw gRPC without a gateway when all your clients support gRPC and you want to minimize latency and complexity by removing the translation layer.
Pick the tool that matches your client ecosystem. Don't add a gateway if no one needs REST.