107. Case Study: Connecting a gRPC-Web (JavaScript) Frontend to a Go Backend
107. Case Study: Connecting a gRPC-Web (JavaScript) Frontend to a Go Backend
Over the past few years, the communication patterns between frontend and backend applications have continued to evolve. REST APIs have certainly been the darling of the industry, but that pattern is increasingly being “challenged” by a more efficient and modern communication protocol called gRPC. One of gRPC’s strengths is its support for protobuf (Protocol Buffers), which enables high-performance data communication between generated code.
However, adopting gRPC on the web frontend isn’t as smooth as it is on mobile or the backend. Browsers don’t directly support HTTP/2, the standard protocol for pure gRPC. This is exactly where gRPC-Web comes in.
This article walks through a concrete case study: connecting a JavaScript frontend (React) that uses gRPC-Web to a Go backend running a conventional gRPC server. I’ll share the step-by-step flow, code walkthroughs, request simulations, and best practices for this kind of development.
Why gRPC-Web?
Before we dive into the code, let’s briefly discuss why we’d go through the trouble of using gRPC instead of plain REST:
| REST | gRPC |
|---|---|
| Data format: text (JSON) | Data format: binary (protobuf) |
| Serialization overhead | Highly efficient, extensible serialization |
| No direct contract | Strictly defined via .proto (contract) |
| HTTP/1.1 | HTTP/2 (faster, multiplexed) |
| Less ideal for streaming | Supports full-duplex streaming |
| Easy to access in the browser | Requires extras (gRPC-Web) |
In modern single-page applications, sending and receiving large, up-to-date, and efficient data becomes important. gRPC on the backend makes this very easy, but a browser-based frontend needs an adjustment via gRPC-Web.
Architecture Diagram
Before the code, here’s the flow diagram:
flowchart TD
Client[gRPC-Web aplikasi JavaScript (Browser)]
-->|request (HTTP/1.1)| Envoy[gRPC-Web Proxy (Envoy)]
-->|translasi ke HTTP/2| GoServer[gRPC Server (Go)]
GoServer
-->|response (HTTP/2)| Envoy
-->|HTTP/1.1 response| Client
So, the data flow is:
- JavaScript client (browser) => sends a request via gRPC-Web (HTTP/1.1).
- The
Envoyproxy (or another plugin) translates the request to HTTP/2 (for the Go gRPC backend). - The response is returned to the browser.
Preparation: Define the Contract (.proto)
The key to a successful gRPC setup is a single source of truth in .proto format. Here’s a simple example in the proto/greet.proto folder:
1syntax = "proto3";
2
3package greet;
4
5service Greeter {
6 rpc SayHello (HelloRequest) returns (HelloReply) {}
7}
8
9message HelloRequest {
10 string name = 1;
11}
12
13message HelloReply {
14 string message = 1;
15}Properties like these form the foundation for both sides—frontend and backend—so they always stay in sync without “guessing” the API!
1. Set Up the gRPC Backend in Go
A. Generate Protobuf
You usually install the Go tooling with:
1go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
2go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestCompile:
1protoc -I proto/ \
2 --go_out=pb --go_opt=paths=source_relative \
3 --go-grpc_out=pb --go-grpc_opt=paths=source_relative \
4 proto/greet.protoB. Implement the Service in Go
File: main.go
1package main
2
3import (
4 "context"
5 "log"
6 "net"
7 "google.golang.org/grpc"
8 pb "your-module/pb/greet"
9)
10
11type server struct {
12 pb.UnimplementedGreeterServer
13}
14
15func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
16 return &pb.HelloReply{Message: "Hello, " + req.Name + "!"}, nil
17}
18
19func main() {
20 lis, err := net.Listen("tcp", ":50051")
21 if err != nil {
22 log.Fatalf("Failed to listen: %v", err)
23 }
24 s := grpc.NewServer()
25 pb.RegisterGreeterServer(s, &server{})
26 log.Println("gRPC Go server running on :50051")
27 if err := s.Serve(lis); err != nil {
28 log.Fatalf("Failed to serve: %v", err)
29 }
30}2. Set Up the gRPC-Web Proxy (Envoy)
Since browsers can only speak HTTP/1.1 + CORS, we need a proxy. One battle-proven option is Envoy. Here’s a minimal configuration example (envoy.yaml):
1static_resources:
2 listeners:
3 - address:
4 socket_address:
5 address: 0.0.0.0
6 port_value: 8080
7 filter_chains:
8 - filters:
9 - name: envoy.filters.network.http_connection_manager
10 typed_config:
11 "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
12 codec_type: auto
13 stat_prefix: ingress_http
14 route_config:
15 name: local_route
16 virtual_hosts:
17 - name: backend
18 domains: ["*"]
19 routes:
20 - match:
21 prefix: "/"
22 route:
23 cluster: greeter_service
24 http_filters:
25 - name: envoy.filters.http.grpc_web
26 - name: envoy.filters.http.cors
27 - name: envoy.filters.http.router
28 clusters:
29 - name: greeter_service
30 connect_timeout: 0.25s
31 type: logical_dns
32 lb_policy: round_robin
33 http2_protocol_options: {}
34 load_assignment:
35 cluster_name: greeter_service
36 endpoints:
37 - lb_endpoints:
38 - endpoint:
39 address:
40 socket_address:
41 address: host.docker.internal
42 port_value: 50051Run it with Docker:
1docker run --rm -it -v $(pwd)/envoy.yaml:/etc/envoy/envoy.yaml -p 8080:8080 envoyproxy/envoy:v1.21-latestIf the Go backend is running on the host, use host.docker.internal in the Envoy cluster.
3. Set Up the gRPC-Web Frontend (JavaScript/React)
A. Generate the JS Stub from .proto
Use the protoc-gen-grpc-web plugin:
1npm install -g protoc-gen-grpc-web
2protoc -I=proto \
3 greet.proto \
4 --js_out=import_style=commonjs:./src/pb \
5 --grpc-web_out=import_style=commonjs,mode=grpcwebtext:./src/pbIn package.json, add the devDependencies:
1"dependencies": {
2 "grpc-web": "^1.4.2"
3}B. Create the gRPC-Web Client
File: src/App.js
1import React, { useState } from 'react';
2import { GreeterClient } from './pb/greet_grpc_web_pb';
3import { HelloRequest } from './pb/greet_pb';
4
5function App() {
6 const [name, setName] = useState('');
7 const [reply, setReply] = useState('');
8
9 const client = new GreeterClient('http://localhost:8080', null, null);
10
11 const sayHello = () => {
12 const request = new HelloRequest();
13 request.setName(name);
14 client.sayHello(request, {}, (err, response) => {
15 if (err) {
16 setReply('Error: ' + err.message);
17 } else {
18 setReply(response.getMessage());
19 }
20 });
21 };
22
23 return (
24 <div>
25 <h1>gRPC-Web Demo: Say Hello</h1>
26 <input value={name} onChange={e => setName(e.target.value)} />
27 <button onClick={sayHello}>Send</button>
28 <p>Server replies: {reply}</p>
29 </div>
30 );
31}
32
33export default App;4. Simulating a Request with the Client
Say the user types “Kevin” and submits. Here’s the request-response flow (simulated from the network devtools):
- HTTP/1.1 POST to
http://localhost:8080/greet.Greeter/SayHello
Content-Type: application/grpc-web-text - Envoy receives it and forwards it to the Go gRPC server via HTTP/2.
- The backend replies with the message
"Hello, Kevin!" - Envoy encodes the response into the grpc-web format and delivers it to the browser.
Troubleshooting & Best Practices
| Problem | Cause & Solution |
|---|---|
| CORS error in the JS client | Make sure the Envoy cors filter is enabled and configure allow_origin |
UNAVAILABLE error on the client | The Envoy proxy isn’t connected to the Go backend; check the IP/port |
| Unexpected content-type | Make sure the client uses grpcwebtext mode and the generated JS stub |
| Go server doesn’t handle the request | Confirm the service is registered correctly; check the Go server logs |
Conclusion
With the architecture above, we can experience the power of gRPC in the browser frontend despite the limitations of HTTP/2 and CORS. Communication between services becomes more standardized, safe from API mismatches, and far more maintainable over the long term!
The actual process of adopting gRPC-Web like this is admittedly a bit more “engineering-heavy” than the REST pattern. But for enterprise applications, microservices, and real-time systems, the investment is well worth it.
I hope this case study helps you bring together a modern JavaScript frontend and a gRPC-based Go backend, ready to build web systems that are scalable, modular, and future-proof. Happy experimenting!
References