Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
23 Sep 2025 · 6 min read ·Article 107 / 110
Go

107. Case Study: Connecting a gRPC-Web (JavaScript) Frontend to a Go Backend

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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:

RESTgRPC
Data format: text (JSON)Data format: binary (protobuf)
Serialization overheadHighly efficient, extensible serialization
No direct contractStrictly defined via .proto (contract)
HTTP/1.1HTTP/2 (faster, multiplexed)
Less ideal for streamingSupports full-duplex streaming
Easy to access in the browserRequires 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:

MERMAID
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:

  1. JavaScript client (browser) => sends a request via gRPC-Web (HTTP/1.1).
  2. The Envoy proxy (or another plugin) translates the request to HTTP/2 (for the Go gRPC backend).
  3. 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:

protobuf
 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:

sh
1go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
2go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Compile:

sh
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.proto

B. Implement the Service in Go

File: main.go

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):

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: 50051

Run it with Docker:

sh
1docker run --rm -it -v $(pwd)/envoy.yaml:/etc/envoy/envoy.yaml -p 8080:8080 envoyproxy/envoy:v1.21-latest

If 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:

sh
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/pb

In package.json, add the devDependencies:

json
1"dependencies": {
2    "grpc-web": "^1.4.2"
3}

B. Create the gRPC-Web Client

File: src/App.js

javascript
 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):

  1. HTTP/1.1 POST to http://localhost:8080/greet.Greeter/SayHello
    Content-Type: application/grpc-web-text
  2. Envoy receives it and forwards it to the Go gRPC server via HTTP/2.
  3. The backend replies with the message "Hello, Kevin!"
  4. Envoy encodes the response into the grpc-web format and delivers it to the browser.

Troubleshooting & Best Practices

ProblemCause & Solution
CORS error in the JS clientMake sure the Envoy cors filter is enabled and configure allow_origin
UNAVAILABLE error on the clientThe Envoy proxy isn’t connected to the Go backend; check the IP/port
Unexpected content-typeMake sure the client uses grpcwebtext mode and the generated JS stub
Go server doesn’t handle the requestConfirm 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

Related Articles

💬 Comments