How to use gRPC-web with React for High-Performance Apps

gRPC is a high-performance, open-source universal RPC framework created by Google. It uses HTTP/2 for transport, Protocol Buffers as the interface description language, and provides features such as authentication, bidirectional streaming and flow control, blocking or nonblocking bindings, and cancellation and timeouts.

Since its release in 2015, gRPC has seen significant adoption for building efficient and scalable APIs. According to the 2019 Postman API survey, 15% of developers reported using gRPC for their APIs, and another 19% said they were planning to use it in the next 12 months. High-profile companies like Netflix, Square, and Cisco have all written about their successful experiences with gRPC.

So why is gRPC so popular? Compared to traditional RESTful APIs, gRPC offers several advantages:

  1. Performance: gRPC uses HTTP/2 for transport, which supports multiplexed streaming and header compression. This eliminates the overhead of multiple HTTP 1.1 connections and reduces network usage. In benchmarks, gRPC has been shown to be up to 8x faster than REST for receiving messages.

  2. Code Generation: gRPC uses Protocol Buffers to define service contracts. With protobuf, you specify your messages and services in a .proto file and the compiler generates client and server code stubs in your preferred language. This eliminates a lot of boilerplate and keeps your client and server in sync.

  3. Streaming: gRPC supports client, server, and bi-directional streaming, which enables real-time push notifications and more complex interaction patterns beyond typical request-response.

  4. Interoperability: gRPC has first-class support in 11 languages and platforms, making it easy to create multi-language microservices that communicate with each other.

However, one limitation of gRPC is that it was originally designed for internal server-to-server communication, not for use in web browsers. Modern browsers support HTTP/2, but cannot directly make gRPC requests for several reasons:

  1. Browsers do not expose the low-level HTTP/2 primitives required by gRPC
  2. Browser APIs like fetch and XMLHttpRequest are designed around HTTP 1.1 semantics and do not support HTTP/2 features like streams
  3. The gRPC wire format is not compatible with the browser‘s built-in encoding and compression algorithms

Enter gRPC-web

To enable gRPC for the browser, Google developed the gRPC-web protocol and library. gRPC-web lets you define and consume gRPC services from JavaScript code running in a web browser.

Instead of trying to support HTTP/2 directly in the browser, gRPC-web works by exposing gRPC services over HTTP 1.1 via a special proxy. The proxy translates between the gRPC HTTP 2 protocol and gRPC-web‘s HTTP 1.1 protocol.

Here‘s a simplified diagram of the gRPC-web architecture:

+---------+   HTTP 1.1   +-----------+   HTTP 2   +--------------+
| Browser | -----------> |    Envoy  | ---------> | gRPC Backend |
|         | <-----------  |    Proxy  | <--------- |              |  
+---------+   gRPC-web    +-----------+     gRPC   +--------------+

           Payload in      Proxies request      Speaks native 
            binary ProtoBuf    to HTTP 2           gRPC

Some key things to note in this flow:

  1. The browser makes an HTTP 1.1 POST request to the Envoy proxy, with the gRPC service name and method included in the request body
  2. Envoy translates the HTTP 1.1 request into an HTTP 2 request and forwards it to the backend gRPC service
  3. The gRPC backend handles the request and sends an HTTP 2 response back to Envoy
  4. Envoy translates the HTTP 2 response back to HTTP 1.1 and sends it to the browser
  5. The browser receives the HTTP 1.1 response and deserializes the binary protobuf payload

One benefit of this approach is that it requires no changes to existing gRPC backend services. Any gRPC service that uses protocol buffers can be exposed via gRPC-web with just a few configuration changes in Envoy.

Now let‘s see how to use gRPC-web in practice with a React frontend app. We‘ll walk through a complete example step-by-step. You can find the full code in this GitHub repo.

Defining the Service

First, define your gRPC service using protocol buffers. Here‘s a sample helloworld.proto file:

syntax = "proto3";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}  

This defines a simple Greeter service with one unary SayHello RPC method. The method takes a HelloRequest message containing a name and returns a HelloReply message with a personalized greeting.

Generating Client Code

Next, use the protoc compiler to generate the gRPC client stub and message classes for JavaScript:

protoc helloworld.proto \
  --js_out=import_style=commonjs,binary:. \
  --grpc-web_out=import_style=typescript,mode=grpcwebtext:.

This will generate two files:

  • helloworld_pb.js: The protocol buffer code for serializing and deserializing HelloRequest and HelloResponse messages
  • helloworld_grpc_web_pb.js: The client stub code for calling the SayHello method from JavaScript

Configuring the Envoy Proxy

To proxy browser requests to the backend gRPC service, you need to run Envoy with a custom configuration file. Here‘s a minimal envoy.yaml:

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 8080 }
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        config:
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route: { cluster: greeter_service }
          http_filters:
          - name: envoy.grpc_web
          - name: envoy.cors
          - name: envoy.router
  clusters:
  - name: greeter_service
    connect_timeout: 0.25s
    type: logical_dns
    http2_protocol_options: {}
    lb_policy: round_robin
    hosts: [{ socket_address: { address: localhost, port_value: 9090 }}]

This configuration sets up an HTTP listener on port 8080 and proxies all traffic to a cluster named greeter_service running on localhost:9090. It enables the envoy.grpc_web filter to translate between HTTP 1.1 and HTTP 2, and the envoy.cors filter to allow the browser to make cross-origin requests.

You can run Envoy with Docker using this command:

docker run -d -p 8080:8080 \
  -v "$(pwd)"/envoy.yaml:/etc/envoy/envoy.yaml \
  envoyproxy/envoy:v1.15.0

Creating the React Component

With the generated client code and Envoy proxy in place, you can now call the gRPC method from a React component. Here‘s a simplified example:

import React, { Component } from ‘react‘;
import { HelloRequest, HelloReply } from ‘./helloworld_pb.js‘;
import { GreeterClient } from ‘./helloworld_grpc_web_pb.js‘;

class Greeter extends Component {
  constructor(props) {
    super(props);
    this.state = { greeting: ‘‘, name: ‘‘ };
  }

  render() {
    return (
      <div>
        <input type="text" 
               value={this.state.name}
               onChange={this.onNameChange.bind(this)} />
        <button onClick={this.sayHello.bind(this)}>Greet</button>
        <p>{this.state.greeting}</p>
      </div>
    );
  }

  onNameChange(event) {
    this.setState({ name: event.target.value });
  }

  sayHello() {
    const client = new GreeterClient(‘http://localhost:8080‘);
    const request = new HelloRequest();
    request.setName(this.state.name);

    client.sayHello(request, {}, (err, response) => {
      if (err) {
        console.error(err);
        return;
      }
      this.setState({ greeting: response.getMessage() });
    });
  }
}

export default Greeter;

In this component:

  1. The component renders a text input for the user‘s name and a button to send the gRPC request
  2. When the button is clicked, the sayHello method is called
  3. sayHello creates a new GreeterClient instance pointing to the Envoy proxy running on localhost:8080
  4. It then creates a new HelloRequest, sets the name field to the value entered by the user, and calls the sayHello method
  5. When the response is received, the callback function is invoked with the HelloReply
  6. The component state is updated with the message from the HelloReply, which triggers a re-render displaying the personalized greeting

With all the pieces in place, you can run the React development server:

npm start

And then navigate to http://localhost:3000 and test the gRPC-web flow end-to-end. Everything should work as expected!

Production Considerations

While this example demonstrates the core concepts, there are additional considerations to keep in mind for production applications:

  1. Secure Connections: By default, Envoy uses plaintext HTTP. For a production deployment, you should configure Envoy with TLS certificates and enable TLS on the backend gRPC services. You can configure Envoy to terminate TLS or to use end-to-end encryption with the backend.

  2. Error Handling: The example code does not have robust error handling. In a real app, you should handle errors returned by the gRPC server and display user-friendly error messages. You can check the grpc-status response header to get the gRPC status code.

  3. Metadata: gRPC supports sending metadata (key-value pairs) along with requests and responses. With gRPC-web, you can send metadata by setting the grpc-meta- request header prefix. On the server side, you can access the metadata in the ctx context object.

  4. Monitoring and Tracing: For monitoring gRPC-web services, you can use standard HTTP monitoring tools, as the requests are proxied over HTTP 1.1. However, to get deeper insights into gRPC performance, you may want to use a dedicated tool like gRPC-web-devtools. For tracing, Envoy supports distributed tracing protocols like Zipkin, Jaeger, and Datadog.

Alternatives to gRPC-web

While gRPC-web is a great fit for many use cases, it‘s not the only way to build APIs for the web. Some alternatives to consider:

  1. REST APIs with JSON: If you don‘t need the performance or streaming features of gRPC, a traditional REST API with JSON payloads may be simpler to develop and consume from the browser. Many web developers are already familiar with REST conventions.

  2. REST APIs with Protocol Buffers: You can get some of the benefits of gRPC (strongly-typed schemas, compact payloads, code generation) while still using HTTP 1.1 by exposing your protocol buffers over a REST API. This is supported by many API frameworks like grpc-gateway.

  3. WebSocket APIs: If you need bidirectional streaming and low-latency communication, you can use WebSockets to establish a persistent connection between the browser and server. Libraries like socket.io and ws make it easy to build real-time web apps.

Ultimately, the choice of API technology should be driven by your specific requirements and constraints. gRPC-web is a compelling option if you already have gRPC services and want to reuse them in a browser app, or if you need the performance and type safety benefits of gRPC.

Conclusion

In this post, we took a deep dive into gRPC-web and how to use it with React. gRPC-web brings the performance and engineering benefits of gRPC to browser apps, enabling a new generation of highly efficient and scalable web APIs.

While there is some additional complexity in setting up the Envoy proxy and generating the client code, the benefits are significant. With gRPC-web, you get strongly-typed APIs, compact binary payloads, and the ability to reuse backend gRPC services without any modifications.

If you‘re building a new web app from scratch or looking to modernize an existing API, I highly recommend giving gRPC-web a try. It‘s a powerful tool to have in your toolkit as a full-stack developer.

To learn more about gRPC-web, check out the following resources:

You can find the complete code for the React gRPC-web example in this GitHub repository. Feel free to use it as a starting point for your own projects.

If you have any questions or feedback, let me know in the comments below. Thanks for reading!

Similar Posts