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

73. gRPC-Web for Communicating with the Frontend (React/Vue)

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

73. gRPC-Web for Communicating with the Frontend (React/Vue)

In the modern era of web applications, the need for efficient and reactive communication between the frontend and backend is becoming increasingly important. Traditionally, REST-based APIs using JSON have been the primary choice. Today, however, more and more engineering teams are turning to gRPC as an alternative offering low latency and high efficiency thanks to Protobuf. That said, gRPC essentially runs on top of HTTP/2—which not all browsers support natively for gRPC. This is where gRPC-Web steps in as a bridging technology that allows browsers (React/Vue) to communicate with a gRPC backend.

In this article, I’ll cover the concept of gRPC-Web, how to configure it, and an example implementation on a React/Vue frontend that interacts with a gRPC backend. Let’s start with a brief introduction.


What Are gRPC and gRPC-Web?

gRPC in Brief

gRPC is an HTTP/2-based remote procedure call framework developed by Google. It uses Protocol Buffers (“Protobuf”) as its data serialization format, which is more compact and faster than JSON. gRPC is well known for its Contract First, Multi-language (Polyglot), and streaming (bidirectional as well as server/client streaming) capabilities.

gRPC’s Limitations in the Browser

Unfortunately, browsers today do not fully support the features required by the gRPC protocol, particularly HTTP/2 framing and headers. Even the gRPC package available for Node.js cannot be bundled into the browser. This is where gRPC-Web becomes the solution.

gRPC-Web: A Bridge for the Browser

gRPC-Web is a project that enables web applications (React, Vue, Angular, and so on) to call gRPC backend APIs. It provides a JavaScript library and a simple wire protocol over HTTP/1.1/2 with CORS, so that frontend applications can communicate with a gRPC backend.


gRPC-Web Architecture

Let’s visualize the architecture:

MERMAID
flowchart LR
    A[React/Vue SPA
gRPC-Web Client] --protokol gRPC-Web--> B[gRPC-Web Proxy] B --gRPC--> C[gRPC Backend Service]
  • React/Vue SPA: The browser uses a gRPC-Web client to make requests.
  • gRPC-Web Proxy: Tools such as Envoy or a standalone proxy translate HTTP/1.1 gRPC-Web requests into HTTP/2 gRPC to forward them to the gRPC backend.
  • gRPC Backend Service: A microservice/backend runs the gRPC server.

A Simple Case Study: Todo Service

1. Define the Protobuf

Suppose we want to build a simple Todo application. Here’s the todo.proto file:

proto
 1syntax = "proto3";
 2
 3package todo;
 4
 5service TodoService {
 6  rpc ListTodos (Empty) returns (TodoListResponse);
 7  rpc AddTodo (AddTodoRequest) returns (TodoResponse);
 8}
 9
10message Empty {}
11
12message AddTodoRequest {
13  string title = 1;
14}
15
16message Todo {
17  int32 id = 1;
18  string title = 2;
19  bool completed = 3;
20}
21
22message TodoResponse {
23  Todo todo = 1;
24}
25
26message TodoListResponse {
27  repeated Todo todos = 1;
28}

2. Generate the Stubs

Use the protoc-gen-grpc-web plugin to generate the JS/TS stubs:

bash
1protoc -I=./proto todo.proto \
2    --js_out=import_style=commonjs:./frontend/src/proto \
3    --grpc-web_out=import_style=typescript,mode=grpcwebtext:./frontend/src/proto

3. Set Up the gRPC Backend

Suppose we implement the backend in Node.js (or Go). The key point is that this backend runs the TodoService service on a specific port (for example, 50051). In this article, we’ll focus on bridging it to the frontend.

4. Deploy the gRPC-Web Proxy

Using Envoy (the most common approach):

An envoy.yaml config snippet:

yaml
 1static_resources:
 2  listeners:
 3    - name: listener_0
 4      address:
 5        socket_address: { address: 0.0.0.0, port_value: 8080 }
 6      filter_chains:
 7        - filters:
 8            - name: envoy.filters.network.http_connection_manager
 9              typed_config:
10                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
11                codec_type: AUTO
12                stat_prefix: ingress_http
13                route_config:
14                  name: local_route
15                  virtual_hosts:
16                    - name: backend
17                      domains: ["*"]
18                      routes:
19                        - match: { prefix: "/" }
20                          route: { cluster: grpc_backend }
21                http_filters:
22                  - name: envoy.filters.http.grpc_web
23                  - name: envoy.filters.http.cors
24                  - name: envoy.filters.http.router
25  clusters:
26    - name: grpc_backend
27      connect_timeout: 0.25s
28      type: logical_dns
29      http2_protocol_options: {}
30      lb_policy: round_robin
31      load_assignment:
32        cluster_name: grpc_backend
33        endpoints:
34          - lb_endpoints:
35              - endpoint:
36                  address:
37                    socket_address:
38                      address: host.docker.internal
39                      port_value: 50051
Explanation: Envoy receives requests from the frontend on port 8080 and forwards them to the gRPC backend on port 50051.


Implementation on the Frontend

React: Client Example

  1. Install Dependencies

    bash
    1npm install grpc-web google-protobuf

  2. Implement the Todo Client

src/App.tsx:

tsx
 1import React, { useEffect, useState } from 'react';
 2import { TodoServiceClient } from './proto/todo_grpc_web_pb';
 3import { Empty, AddTodoRequest, Todo } from './proto/todo_pb';
 4
 5const client = new TodoServiceClient('http://localhost:8080', null, null);
 6
 7function App() {
 8  const [todos, setTodos] = useState<Todo.AsObject[]>([]);
 9  const [title, setTitle] = useState('');
10
11  useEffect(() => {
12    const req = new Empty();
13    client.listTodos(req, {}, (err, response) => {
14      if (response) setTodos(response.getTodosList().map(todo => todo.toObject()));
15    });
16  }, []);
17
18  const handleAdd = () => {
19    const req = new AddTodoRequest();
20    req.setTitle(title);
21    client.addTodo(req, {}, (err, response) => {
22      if (response) setTodos(old => [...old, response.getTodo()?.toObject()]);
23      setTitle('');
24    });
25  };
26
27  return (
28    <div>
29      <h1>Todo List (gRPC-Web)</h1>
30      <ul>
31        {todos.map(todo => (
32          <li key={todo.id}>
33            {todo.title} {todo.completed ? "✔" : ""}
34          </li>
35        ))}
36      </ul>
37      <input value={title} onChange={e => setTitle(e.target.value)} />
38      <button onClick={handleAdd}>Add Todo</button>
39    </div>
40  );
41}
42
43export default App;

Vue: The Same Concept

  1. Install the Dependency

    bash
    1npm install grpc-web google-protobuf

  2. Implementation in Vue

src/components/Todo.vue:

vue
 1<template>
 2  <div>
 3    <h1>Todo List (gRPC-Web)</h1>
 4    <ul>
 5      <li v-for="todo in todos" :key="todo.id">{{ todo.title }} {{ todo.completed ? "✔" : "" }}</li>
 6    </ul>
 7    <input v-model="title" />
 8    <button @click="addTodo">Add Todo</button>
 9  </div>
10</template>
11
12<script lang="ts">
13import { defineComponent, ref, onMounted } from 'vue';
14import { TodoServiceClient } from '../proto/todo_grpc_web_pb';
15import { Empty, AddTodoRequest, Todo } from '../proto/todo_pb';
16
17const client = new TodoServiceClient('http://localhost:8080', null, null);
18
19export default defineComponent({
20  setup() {
21    const todos = ref<Todo.AsObject[]>([]);
22    const title = ref('');
23
24    onMounted(() => {
25      const req = new Empty();
26      client.listTodos(req, {}, (err, response) => {
27        if (response) todos.value = response.getTodosList().map(t => t.toObject());
28      });
29    });
30
31    const addTodo = () => {
32      const req = new AddTodoRequest();
33      req.setTitle(title.value);
34      client.addTodo(req, {}, (err, response) => {
35        if (response) todos.value.push(response.getTodo()?.toObject());
36        title.value = '';
37      });
38    };
39
40    return { todos, title, addTodo };
41  }
42});
43</script>

How Does the Performance Compare to REST?

AspectREST (JSON)gRPC-Web (Protobuf)
Payload SizeTends to be large (text)Smaller (binary)
Serialization/DeserializationSlow (JSON.parse)Fast (Protobuf)
Auto-doc/Type-safetyOptional via Swagger/OpenAPIBuilt-in via Proto
StreamingComplicated (Server Sent Events)Native in gRPC/limited on the Web*
Browser SupportUniversalRequires a Proxy (gRPC-Web)
CORS/CredentialBuilt-inRequires config in the proxy

* Note: gRPC-Web only supports unary and server streaming; client streaming is not yet supported.


Conclusion: Is gRPC-Web Right for You?

gRPC-Web unlocks the potential of using gRPC in a modern frontend stack (React/Vue). However, there are trade-offs:

Advantages:

  • Small payloads, fast transmission.
  • Automatic type-safety (from Protobuf to TypeScript/JS).
  • Easy to integrate across multiple platforms.

Drawbacks:

  • Setup and debugging are more complex (you need Envoy/a proxy).
  • Streaming is not yet fully supported (non-bidirectional).
  • Tooling issues: DevTools and error handling are not as clear as with plain fetch/AJAX.

So, if you need high API performance, type-safety, and you have a team familiar with Protobuf/gRPC, gRPC-Web is well worth trying in your React or Vue applications.


References

Happy experimenting, and don’t forget to explore! Share your experience using gRPC-Web in your next frontend project in the comments :)

Related Articles

💬 Comments