15 - gRPC Gateway

📋 Jump to Takeaways

You built a gRPC service. But some clients need REST. Browsers, third-party integrations, curl-wielding developers. gRPC-Gateway generates a reverse proxy that translates RESTful JSON requests into gRPC calls. One service, two protocols.

How It Works

gRPC-Gateway is a protoc plugin. You annotate your .proto file with HTTP mappings, run the code generator, and get a Go HTTP handler that proxies requests to your gRPC server.

Browser/curl → HTTP/JSON → gRPC-Gateway → gRPC/protobuf → Your Server

The gateway handles JSON serialization, HTTP method routing, and path parameter extraction. Your gRPC server doesn't change at all.

Installing the Gateway Plugin

go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest

Add the gateway runtime to your module:

go get github.com/grpc-ecosystem/grpc-gateway/v2/runtime

You also need the Google API annotations:

# In your project root
mkdir -p google/api
curl -L https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/annotations.proto \
  -o google/api/annotations.proto
curl -L https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto \
  -o google/api/http.proto

Annotate the Proto File

Add HTTP mappings to your service methods:

syntax = "proto3";

package shortener;

option go_package = "shortener/pb";

import "google/api/annotations.proto";

message Link {
  int64 id = 1;
  string url = 2;
  string short_code = 3;
  int64 clicks = 4;
}

message CreateLinkRequest {
  string url = 1;
}

message CreateLinkResponse {
  Link link = 1;
}

message GetLinkRequest {
  string short_code = 1;
}

message GetLinkResponse {
  Link link = 1;
}

message DeleteLinkRequest {
  string short_code = 1;
}

message DeleteLinkResponse {}

message ListLinksRequest {}

message BatchCreateLinksResponse {
  int64 created_count = 1;
  repeated Link links = 2;
}

message SyncRequest {
  string short_code = 1;
  int64 clicks = 2;
}

message SyncResponse {
  string short_code = 1;
  int64 total_clicks = 2;
  bool acknowledged = 3;
}

service LinkService {
  rpc CreateLink(CreateLinkRequest) returns (CreateLinkResponse) {
    option (google.api.http) = {
      post: "/v1/links"
      body: "*"
    };
  }

  rpc GetLink(GetLinkRequest) returns (GetLinkResponse) {
    option (google.api.http) = {
      get: "/v1/links/{short_code}"
    };
  }

  rpc DeleteLink(DeleteLinkRequest) returns (DeleteLinkResponse) {
    option (google.api.http) = {
      delete: "/v1/links/{short_code}"
    };
  }

  rpc ListLinks(ListLinksRequest) returns (stream Link) {
    option (google.api.http) = {
      get: "/v1/links"
    };
  }

  // Streaming RPCs don't get HTTP annotations — they only work over gRPC
  rpc BatchCreateLinks(stream CreateLinkRequest) returns (BatchCreateLinksResponse);
  rpc SyncClicks(stream SyncRequest) returns (stream SyncResponse);
}

Why no annotations on streaming RPCs? Client streaming and bidirectional streaming cannot be mapped to REST endpoints through gRPC-Gateway. HTTP/1.1 doesn't support streaming requests from the client. Server streaming works (the gateway returns newline-delimited JSON), but client and bidirectional streaming are gRPC-only. Clients that need those must use the gRPC client directly.

The {short_code} in the URL path maps to the short_code field in the request message. body: "*" means the entire request body maps to the request message.

Generate the Gateway Code

Update your Makefile:

.PHONY: proto
proto:
	mkdir -p pb
	protoc \
		--proto_path=proto \
		--proto_path=. \
		--go_out=pb --go_opt=paths=source_relative \
		--go-grpc_out=pb --go-grpc_opt=paths=source_relative \
		--grpc-gateway_out=pb --grpc-gateway_opt=paths=source_relative \
		proto/*.proto

The second --proto_path=. lets protoc find google/api/annotations.proto from the project root. Two include paths are needed because:

  • --proto_path=proto — resolves your link.proto (so output files land in pb/ not pb/proto/)
  • --proto_path=. — resolves import "google/api/annotations.proto" which lives at ./google/api/

Protoc searches them in order. Multiple --proto_path flags is the standard way to handle imports from different directories.

This generates a third file: link.pb.gw.go containing the HTTP handler.

After regenerating, run go mod tidy to resolve the new dependencies. Your existing server (go run .) and client (go run ./cmd/client/) should still work as before.

Gateway Startup

The gateway runs as a separate process that proxies HTTP requests to your gRPC server. Create cmd/gateway/main.go:

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"

    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"

    "shortener/pb"
)

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

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

    fmt.Println("HTTP gateway on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

Run your gRPC server first (go run .), then the gateway in a second terminal:

go run ./cmd/gateway/

You need two terminals running:

  1. Terminal 1 — gRPC server: go run .
  2. Terminal 2 — HTTP gateway: go run ./cmd/gateway/

The gateway proxies to localhost:50051. If the server isn't running, you'll get a "connection refused" error.

Now you have both:

  • gRPC on port 50051 (your existing server)
  • REST on port 8080 (the gateway proxy)

Test with curl

# Create a link
curl -X POST http://localhost:8080/v1/links \
  -H "Content-Type: application/json" \
  -d '{"url": "https://golang.org"}'

# Get a link
curl http://localhost:8080/v1/links/abc123

# Delete a link
curl -X DELETE http://localhost:8080/v1/links/abc123

# List all links
curl http://localhost:8080/v1/links

The gateway translates JSON to protobuf, calls the gRPC server, and translates the response back to JSON. Your gRPC handlers don't know or care that the request came from HTTP.

Streaming responses: ListLinks is a server-streaming RPC. The gateway returns newline-delimited JSON (NDJSON) — one JSON object per line, not a JSON array. Each line is a result wrapper:

{"result":{"id":"1","url":"https://example.com","shortCode":"abc123","clicks":"0"}}
{"result":{"id":"2","url":"https://golang.org","shortCode":"def456","clicks":"0"}}

Custom JSON Marshaling

By default, the gateway uses protojson which outputs camelCase field names. You can customize this in cmd/gateway/main.go:

import "google.golang.org/protobuf/encoding/protojson"

mux := runtime.NewServeMux(
    runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{
        MarshalOptions: protojson.MarshalOptions{
            UseProtoNames: true, // use snake_case instead of camelCase
        },
        UnmarshalOptions: protojson.UnmarshalOptions{
            DiscardUnknown: true, // ignore unknown fields in requests
        },
    }),
)

Error Mapping

gRPC status codes map to HTTP status codes automatically:

gRPC Code HTTP Status
OK 200
InvalidArgument 400
Unauthenticated 401
PermissionDenied 403
NotFound 404
AlreadyExists 409
Internal 500
Unavailable 503

You don't configure this. The gateway handles it.

Key Takeaways

  • gRPC-Gateway generates an HTTP reverse proxy from annotated .proto files
  • Annotate RPCs with google.api.http options to define REST endpoints
  • Path parameters like {short_code} map to request message fields
  • The gateway runs as a separate HTTP server that proxies to your gRPC server
  • gRPC status codes map to HTTP status codes automatically
  • Your gRPC handlers don't change, the gateway handles the translation
  • Use this when you need both gRPC for internal services and REST for external clients

💻 Examples

Complete examples for this lesson. Copy and run locally.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue

© 2026 ByteLearn.dev. Free courses for developers. · Privacy