The two-protocol problem
You built a gRPC service. Your internal microservices love it. The binary Protocol Buffers are fast, the strict schema catches bugs early, and streaming works beautifully. Then the frontend team asks for a REST API for the mobile app. Or a third-party partner wants to integrate via a standard JSON webhook.
Writing a separate REST service duplicates your business logic. Maintaining two endpoints for the same data leads to drift. You end up fixing a bug in the gRPC handler and forgetting the REST handler, or vice versa. gRPC-Gateway solves this by generating a REST proxy from your .proto files. You define the service once. The gateway handles the translation between HTTP/JSON and gRPC automatically.
What gRPC-Gateway actually does
gRPC uses HTTP/2 and binary Protocol Buffers. REST clients expect HTTP/1.1 or HTTP/2 with JSON payloads. These protocols speak different languages. gRPC-Gateway is a reverse proxy that translates between them.
Think of a high-speed rail station. The trains (gRPC) run on a dedicated track with a precise schedule and special tickets (Protobufs). The buses (REST) pick up passengers from the street with standard tickets (JSON). The station master is the gateway. The station master takes a bus passenger, checks their ticket, converts it to a train ticket, escorts them to the platform, and brings the response back. The station master doesn't drive the train. The station master just handles the conversion so the passenger never has to learn the train system.
The gateway is generated code. You don't run a black-box binary. You run protoc with the gRPC-Gateway plugin, and it produces Go source files. These files implement http.Handler. You register them in your Go application alongside your gRPC server. The generated code parses the HTTP request, unmarshals the JSON, calls your gRPC method, marshals the response, and writes the HTTP reply.
Defining the mapping
The translation rules live in your .proto files. You use annotations from google/api/annotations.proto to tell the gateway how to map HTTP paths and methods to gRPC RPCs.
syntax = "proto3";
package example.v1;
option go_package = "github.com/example/api/example/v1";
// Import the annotations package for HTTP mapping.
// This provides the google.api.http option.
import "google/api/annotations.proto";
// User represents a user in the system.
// The json_name option controls the JSON key name.
// Without this, the JSON key would be "user_id" (snake_case).
// Setting json_name makes the JSON key "userId" (camelCase).
message User {
string user_id = 1 [json_name = "userId"];
string email = 2;
}
// CreateUserRequest is the input for creating a user.
message CreateUserRequest {
string email = 1;
}
// CreateUserResponse returns the created user.
message CreateUserResponse {
User user = 1;
}
// UserService defines the RPCs for user management.
service UserService {
// Create creates a new user.
// The option maps POST /v1/users to this RPC.
// The body option specifies that the request body maps to the entire message.
rpc Create(CreateUserRequest) returns (CreateUserResponse) {
option (google.api.http) = {
post: "/v1/users"
body: "*"
};
}
// Get retrieves a user by ID.
// The path parameter {user_id} binds to the user_id field in the request.
rpc Get(GetUserRequest) returns (GetUserResponse) {
option (google.api.http) = {
get: "/v1/users/{user_id}"
};
}
}
message GetUserRequest {
string user_id = 1 [json_name = "userId"];
}
message GetUserResponse {
User user = 1;
}
The annotation option (google.api.http) is the bridge. The post: "/v1/users" line tells the gateway to accept POST requests at that path. The body: "*" line tells the gateway to unmarshal the entire JSON body into the request message. Path parameters like {user_id} extract values from the URL and populate the corresponding field in the request message.
The gateway is a proxy. Put business logic in the service, not the annotations.
Generating the code
You generate the gateway code using protoc with the grpc-gateway plugin. The command looks similar to generating gRPC code, but with an extra output flag.
protoc -I. \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
--grpc-gateway_out=. --grpc-gateway_opt=paths=source_relative \
--grpc-gateway_opt=logtostderr=true \
example/v1/user.proto
The --grpc-gateway_out flag invokes the gateway plugin. The paths=source_relative option ensures the generated Go files respect your directory structure. The logtostderr option configures the generated code to log errors to stderr, which matches Go's standard logging behavior.
The plugin generates a file like user.pb.gw.go. This file contains a function RegisterUserServiceHandlerFromEndpoint. This function takes an http.ServeMux and a gRPC endpoint address. It registers the HTTP handlers on the mux and wires them to the gRPC server.
Trust gofmt. Argue logic, not formatting. Run gofmt -w . after generation to ensure the generated code matches your project style.
Wiring it up in Go
The generated code needs a Go application to run. A common pattern is to run the gRPC server and the gateway on the same port. This avoids managing two separate processes and simplifies deployment.
package main
import (
"context"
"log"
"net"
"net/http"
"time"
"github.com/example/api/example/v1"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// Run starts the gRPC server and the gRPC-Gateway on the same port.
// This pattern allows a single process to serve both gRPC and REST clients.
func Run(ctx context.Context, addr string) error {
// Create the gRPC server.
// The gRPC server handles the actual business logic.
grpcServer := grpc.NewServer()
// Register the service implementation on the gRPC server.
// This is where your actual code lives.
v1.RegisterUserServiceServer(grpcServer, &userServiceImpl{})
// Start the gRPC server in a goroutine.
// The listener accepts connections on the given address.
lis, err := net.Listen("tcp", addr)
if err != nil {
return err
}
go func() {
if err := grpcServer.Serve(lis); err != nil {
log.Printf("gRPC server error: %v", err)
}
}()
// Create the HTTP mux for the gateway.
// The runtime.NewServeMux creates a mux that handles HTTP requests
// and forwards them to the gRPC endpoint.
mux := runtime.NewServeMux()
// Register the gateway handlers.
// This function connects the HTTP mux to the gRPC server.
// It uses the same address as the gRPC server because they share the port.
// The DialOption uses insecure credentials because the connection is local.
err = v1.RegisterUserServiceHandlerFromEndpoint(ctx, mux, addr, []grpc.DialOption{grpc.WithTransportCredentials(insecure.Credentials)})
if err != nil {
return err
}
// Create the HTTP server.
// The HTTP server serves the gateway mux.
// It handles timeouts and graceful shutdown.
httpServer := &http.Server{
Addr: addr,
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Start the HTTP server.
// It listens on the same address as the gRPC server.
// HTTP/2 allows both protocols to multiplex over the same connection.
return httpServer.Serve(lis)
}
// userServiceImpl implements the UserService interface.
// This is where you write your business logic.
type userServiceImpl struct {
v1.UnimplementedUserServiceServer
}
// Create implements the Create RPC.
// The context is always the first parameter by convention.
func (s *userServiceImpl) Create(ctx context.Context, req *v1.CreateUserRequest) (*v1.CreateUserResponse, error) {
// Business logic goes here.
// Return the response and any error.
// The gateway will map the error to an HTTP status code.
return &v1.CreateUserResponse{
User: &v1.User{
UserId: "generated-id",
Email: req.Email,
},
}, nil
}
// Get implements the Get RPC.
func (s *userServiceImpl) Get(ctx context.Context, req *v1.GetUserRequest) (*v1.GetUserResponse, error) {
// Business logic goes here.
return &v1.GetUserResponse{
User: &v1.User{
UserId: req.UserId,
Email: "user@example.com",
},
}, nil
}
The RegisterUserServiceHandlerFromEndpoint function is the key. It registers the HTTP handlers on the mux. When a request arrives, the handler parses the path, unmarshals the JSON, creates a gRPC client, calls the RPC, and writes the response. The grpc.WithTransportCredentials(insecure.Credentials) option is safe here because the gateway connects to the gRPC server on the same machine via a local loopback connection.
The receiver name is usually one or two letters matching the type. Use (s *userServiceImpl) not (this *userServiceImpl).
JSON naming and conventions
Protobuf fields use snake_case by default. JSON clients often expect camelCase. The json_name option in the .proto file controls the JSON key name. Without it, the gateway generates JSON with snake_case keys.
{
"user_id": "123",
"email": "user@example.com"
}
With json_name = "userId", the output becomes:
{
"userId": "123",
"email": "user@example.com"
}
Set json_name on every field that appears in the API. This keeps the JSON consistent with JavaScript and TypeScript conventions. The gateway respects this option during marshaling and unmarshaling.
The if err != nil { return err } pattern is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. Check errors immediately after every call.
Errors and edge cases
gRPC errors map to HTTP status codes. The gateway uses a standard mapping. INVALID_ARGUMENT becomes 400 Bad Request. NOT_FOUND becomes 404 Not Found. ALREADY_EXISTS becomes 409 Conflict. UNAUTHENTICATED becomes 401 Unauthorized. PERMISSION_DENIED becomes 403 Forbidden.
If you return a generic error, the gateway maps it to 500 Internal Server Error. Use status.Error from google.golang.org/grpc/status to return specific gRPC status codes. The gateway translates these to the correct HTTP codes.
import "google.golang.org/grpc/status"
// Get returns a 404 if the user is not found.
func (s *userServiceImpl) Get(ctx context.Context, req *v1.GetUserRequest) (*v1.GetUserResponse, error) {
user, err := s.store.Find(req.UserId)
if err != nil {
// Return a NOT_FOUND status.
// The gateway maps this to HTTP 404.
return nil, status.Error(codes.NotFound, "user not found")
}
return &v1.GetUserResponse{User: user}, nil
}
Path conflicts cause runtime panics. If you define two RPCs with the same HTTP path and method, the generated registration code may panic when you call Register...HandlerFromEndpoint. The compiler won't catch this. The error appears at startup. Check your annotations carefully. Ensure every path is unique for a given method.
Forget to import the runtime package and you get undefined: runtime from the compiler. Forget to use one and you get imported and not used. The gateway requires github.com/grpc-ecosystem/grpc-gateway/v2/runtime.
Goroutine leaks happen when the gateway waits on a channel that never gets closed. Always have a cancellation path. Pass a context.Context with a timeout or cancellation to the registration function.
The worst goroutine bug is the one that never logs. Add logging to your handlers to track request flow.
When to use gRPC-Gateway
Use gRPC-Gateway when you need a public REST API but want to keep business logic in gRPC. Use gRPC-Gateway when you have multiple clients with different protocol needs, such as mobile apps and internal services. Use gRPC-Gateway when you want a single source of truth for your API contract. Use raw gRPC when all clients are internal services that support HTTP/2 and Protobufs. Use a separate REST service when the REST API has completely different logic or data shapes than the gRPC service. Use a Backend for Frontend pattern when the frontend needs a highly tailored API that aggregates multiple services.
One codebase, two faces. Keep the contract in protobufs and let the gateway handle the rest.