Exploring the Power of gRPC-Gateway for Writing RESTful Services
30th June 2026
Rajiv Ranjan Singh
Rajiv Ranjan Singh
A protoc plugin that generates a reverse-proxy server which translates RESTful HTTP API into gRPC
Generate REST API from protobuf definitions
Serve both gRPC and REST from the same service
Multiple ways to do HTTP mappings
Automatic OpenAPI/Swagger documentation
type: google.api.Service config_version: 3 http: rules: - selector: hello.v1.GreeterService.SayHello post: /v1/hello body: "*"
version: v2 modules: - path: proto name: buf.build/wearedevelopers-world-congress-europe-2026/hello deps: - buf.build/googleapis/googleapis lint: use: - STANDARD - FILE_LOWER_SNAKE_CASE ignore: # Ignore the response naming convention for streaming RPCs - RPC_RESPONSE_STANDARD_NAME
version: v2 managed: enabled: true disable: - module: buf.build/googleapis/googleapis override: - file_option: go_package_prefix value: github.com/iamrajiv/wearedevelopers-world-congress-europe-2026/examples/hello/gen/proto plugins: - remote: buf.build/protocolbuffers/go:v1.36.11 out: gen/proto opt: - paths=source_relative - remote: buf.build/grpc/go:v1.5.1 out: gen/proto opt: - paths=source_relative - remote: buf.build/grpc-ecosystem/gateway:v2.29.0 out: gen/proto opt: - paths=source_relative
type server struct { hellopb.UnimplementedGreeterServiceServer }
func runGRPCServer() error { lis, err := net.Listen("tcp", ":50051") if err != nil { return fmt.Errorf("failed to listen: %v", err) } grpcServer := grpc.NewServer() hellopb.RegisterGreeterServiceServer(grpcServer, &server{}) // Register reflection service for grpcurl reflection.Register(grpcServer) log.Println("gRPC server listening on :50051") if err := grpcServer.Serve(lis); err != nil { return fmt.Errorf("failed to serve gRPC: %v", err) } return nil }
func runRESTGateway() error { ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() // Create gRPC-Gateway mux mux := runtime.NewServeMux() // Register the service handler opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} err := hellopb.RegisterGreeterServiceHandlerFromEndpoint(ctx, mux, "localhost:50051", opts) if err != nil { return fmt.Errorf("failed to register gateway: %v", err) } // Start HTTP server log.Println("gRPC-Gateway (REST) server listening on :8080") if err := http.ListenAndServe(":8080", mux); err != nil { return fmt.Errorf("failed to serve REST: %v", err) } return nil }
gRPC Client Test:
grpcurl -plaintext \
-d '{"name": "WeAreDevelopers"}' localhost:50051 hello.v1.GreeterService/SayHello
REST Client Test:
curl -X POST http://localhost:8080/v1/hello \
-H "Content-Type: application/json" -d '{"name": "WeAreDevelopers"}'
Both Return:
{
"message": "Hello, WeAreDevelopers!"
}
19
service GreeterService {
rpc StreamGreetings(StreamRequest) returns (stream HelloResponse) {
option (google.api.http) = {
get: "/v1/greetings/stream"
};
}
}
Returns: NDJSON (newline-delimited JSON) streaming over HTTP using chunked transfer encoding
Streaming Support:
func (s *server) GetUser(ctx context.Context, req *GetUserRequest) (*User, error) { // Simulate database lookup user, err := s.db.QueryRowContext(ctx, "SELECT user_id, name, email, created_at FROM users WHERE user_id = ?", req.UserId).Scan() // Handle not found error if err == sql.ErrNoRows { return nil, status.Errorf(codes.NotFound, "user %s not found", req.UserId) } // Handle database errors if err != nil { return nil, status.Errorf(codes.Internal, "database error: %v", err) } // Validate user data if user == nil { return nil, status.Error(codes.InvalidArgument, "invalid user data") } return user, nil }
Translation:
codes.NotFound → HTTP 404codes.InvalidArgument → HTTP 400codes.Internal → HTTP 500Rajiv Ranjan Singh