Bridging the Browser and gRPC with Gin and gRPC-Web
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Introduction
In modern web development, microservices architectures have become a cornerstone for building scalable and maintainable applications. gRPC, with its high performance, strong typing, and efficient binary protocol based on Protocol Buffers, is an excellent choice for inter-service communication. However, a significant challenge arises when trying to integrate gRPC services directly with web browsers. Browsers inherently understand HTTP/1.1 and JSON, not the HTTP/2 and binary Protobuf format used by gRPC. This impedance mismatch has historically required complex intermediaries or RESTful proxy layers. This article dives into how Gin, a high-performance HTTP web framework for Go, can be combined with gRPC-Web to effectively bridge this gap, enabling browsers to communicate directly with gRPC services. This approach simplifies the architecture, leverages gRPC's benefits end-to-end, and empowers developers to build more robust and efficient full-stack applications.
Core Concepts and Implementation
Before diving into the implementation, let's define the core technologies involved:
Understanding the Key Technologies
- gRPC (gRPC Remote Procedure Calls): A high-performance, open-source universal RPC framework that uses Protocol Buffers as its Interface Definition Language (IDL) and HTTP/2 for transport. It supports various languages, streaming, and efficient serialization, making it ideal for microservices and polyglot environments.
- Protocol Buffers (Protobuf): A language-neutral, platform-neutral, extensible mechanism for serializing structured data. It's smaller, faster, and simpler than XML or JSON for marshaling data, especially for inter-service communication.
- gRPC-Web: A specification and a set of libraries that allow web applications to interact with gRPC services directly from the browser. It acts as a bridge, translating browser-compatible requests (HTTP/1.1, XHR/Fetch) into gRPC messages and vice-versa, often requiring a proxy that understands both gRPC and gRPC-Web.
- Gin: A fast, lightweight, and extensible web framework for Go. It's often used for building RESTful APIs and can serve as an excellent HTTP server for handling gRPC-Web requests.
- Envoy Proxy: A popular open-source edge and service proxy designed for cloud-native applications. It's frequently used as a reverse proxy, load balancer, and API gateway. In the context of gRPC-Web, Envoy can act as the crucial intermediary, translating gRPC-Web requests from the browser into native gRPC for the backend service.
The Principle of Operation
The core idea is that a web browser, unable to speak native gRPC (HTTP/2), communicates with a proxy service using gRPC-Web (HTTP/1.1 with specific headers). This proxy then translates these gRPC-Web requests into standard gRPC calls (HTTP/2) and forwards them to the actual gRPC backend service. The response follows the inverse path. Gin will serve as our standard HTTP server, which will host both the frontend static files and act as the gRPC-Web proxy if we choose to embed the proxy logic within the Go application, or more commonly, it will interact with an external Envoy proxy that handles the gRPC-Web translation.
Let's illustrate with a common setup where Gin hosts the web application and an Envoy proxy handles the gRPC-Web translation.
Step-by-Step Implementation
We will outline the process by building a simple "Greeter" service.
1. Define the Protocol Buffer Service
First, define your gRPC service using Protocol Buffers. Create proto/greeter.proto
:
syntax = "proto3"; option go_package = "github.com/your/repo/greetpb"; package greet; service Greeter { rpc SayHello (HelloRequest) returns (HelloResponse) {} } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; }
Generate Go code for the gRPC service and gRPC-Web client stubs:
# Install protoc and Go gRPC plugins go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest go install github.com/grpc-ecosystem/grpc-web/protoc-gen-go-grpc-web@latest # Generate Go gRPC code protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ proto/greeter.proto # Generate gRPC-Web client stubs (e.g., for TypeScript) protoc --js_out=import_style=commonjs,binary:. --grpc-web_out=import_style=typescript,mode=grpcwebtext:. \ proto/greeter.proto
This generates greetpb/greeter_grpc.pb.go
, greetpb/greeter.pb.go
for the server, and greeter_pb.js
, greeter_grpc_web_pb.d.ts
for the frontend.
2. Implement the gRPC Server in Go
Create a gRPC server using the generated Go code. File: server/main.go
package main import ( "context" "fmt" "log" "net" "google.golang.org/grpc" "google.golang.org/grpc/reflection" // For gRPC bloom service discovery "github.com/your/repo/greetpb" // Replace with your actual path ) type server struct { greetpb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, in *greetpb.HelloRequest) (*greetpb.HelloResponse, error) { log.Printf("Received: %v", in.GetName()) return &greetpb.HelloResponse{Message: "Hello " + in.GetName()}, nil } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() greetpb.RegisterGreeterServer(s, &server{}) reflection.Register(s) // Enable reflection for gRPCurl etc. log.Printf("gRPC server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
Run this server: go run server/main.go
3. Set up the Envoy Proxy
Envoy will front both our Gin application and the gRPC service, handling gRPC-Web translation. Create envoy.yaml
:
static_resources: listeners: - name: listener_0 address: socket_address: protocol: TCP address: 0.0.0.0 port_value: 8080 # Port where browser connects filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http codec_type: AUTO route_config: name: local_route virtual_hosts: - name: backend domains: ["*"] routes: - match: { prefix: "/greet.Greeter" } # gRPC-Web service prefix route: { cluster: greeter_service, timeout: 0s } typed_per_filter_config: envoy.filters.http.grpc_web: {} - match: { prefix: "/" } # Catch-all for Gin's static files/APIs route: { cluster: gin_web_app } http_filters: - name: envoy.filters.http.grpc_web # Enable gRPC-Web filter first typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb - name: envoy.filters.http.cors # Important for browser security typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors allow_origin_string_match: - exact: "*" # Adjust for production allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web max_age: "1728000" - name: envoy.filters.http.router clusters: - name: greeter_service # gRPC backend connect_timeout: 0.25s type: LOGICAL_DNS dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: greeter_service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 # IP of your gRPC server port_value: 50051 # Port of your gRPC server - name: gin_web_app # Gin application connect_timeout: 0.25s type: LOGICAL_DNS dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: gin_web_app endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 # IP of your Gin server port_value: 8081 # Port of your Gin server
To run Envoy (assuming you have Docker installed):
docker run --rm -it -p 8080:8080 -v $(pwd)/envoy.yaml:/etc/envoy/envoy.yaml envoyproxy/envoy:v1.27.0
This starts Envoy, listening on port 8080
. It routes /greet.Greeter
to our gRPC server (port 50051
) and all other requests to our Gin app (port 8081
).
4. Create the Gin Web Server
Gin will serve the static index.html
and any other traditional HTTP API endpoints. File: web/main.go
package main import ( "log" "net/http" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() // Serve static files from the 'public' directory r.Static("/static", "./public") r.LoadHTMLFiles("./public/index.html") r.GET("/", func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{}) }) log.Printf("Gin server listening on :8081") if err := r.Run(":8081"); err != nil { log.Fatalf("failed to run gin server: %v", err) } }
Create a public
directory and place index.html
inside it.
public/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>gRPC-Web Greeter</title> <script src="/static/main.js" defer></script> </head> <body> <h1>gRPC-Web Greeter Example</h1> <input type="text" id="nameInput" placeholder="Enter your name"> <button id="greetButton">Say Hello</button> <p id="response"></p> </body> </html>
Run the Gin server: go run web/main.go
5. Build the Frontend Application
The frontend will use the generated gRPC-Web client to communicate with the Envoy proxy.
Create public/main.js
(or use TypeScript and compile):
import { GreeterClient } from '../greeter_grpc_web_pb.js'; import { HelloRequest } from '../greeter_pb.js'; const greeterClient = new GreeterClient('http://localhost:8080', null, null); // Envoy address document.getElementById('greetButton').addEventListener('click', () => { const name = document.getElementById('nameInput').value; const request = new HelloRequest(); request.setName(name); greeterClient.sayHello(request, {}, (err, response) => { if (err) { console.error('Error calling SayHello:', err.code, err.message); document.getElementById('response').textContent = 'Error: ' + err.message; return; } document.getElementById('response').textContent = response.getMessage(); }); });
To run this client-side code, you'll typically need a module bundler like Webpack or Parcel, as it uses import
statements. For a quick test, you might use a simple http-server
or live-server
and temporarily adjust the import
paths or bundle it:
# Example with Parcel (install: npm install -g parcel-bundler) parcel build public/main.js --out-dir public --public-url /static
Ensure your public/main.js
is accessible at /static/main.js
as configured in Gin.
Application Scenario
This setup is ideal for:
- Single-Page Applications (SPAs): React, Vue, Angular applications can directly consume gRPC services without a custom REST API layer.
- Internal Dashboards/Tools: Building administrative interfaces that communicate with backend microservices efficiently.
- Hybrid Applications: Where some parts of the frontend require high-performance communication with gRPC services, while others might use traditional HTTP APIs (also served by Gin).
- Reducing Boilerplate: By directly using Protobuf schemas, frontends get automatically generated client code, reducing manual API client development.
Advantages of this Approach
- Performance: Leveraging gRPC’s binary serialization and HTTP/2 (between Envoy and backend) for efficient data transfer.
- Strong Typing: Protobuf provides strong typing from backend to frontend, enhancing maintainability and reducing runtime errors.
- Unified Schema: A single source of truth for your API definition (Protobuf) for both gRPC and gRPC-Web clients.
- Reduced Latency: Elimination of JSON parsing/marshalling overhead compared to REST APIs.
- Simplified Architecture: Less custom code for API gateways or specific REST adapters.
Conclusion
By meticulously orchestrating Gin as a web server, an Envoy proxy for gRPC-Web translation, and a gRPC backend, we successfully enable web browsers to directly interact with high-performance gRPC services. This powerful combination unlocks the full potential of gRPC for full-stack architectures, providing strong typing, enhanced performance, and a streamlined development experience. The ability to speak gRPC from the browser fundamentally changes how we design and build modern web applications, bringing us closer to a truly end-to-end gRPC ecosystem.