decorative abstract image of purple background with blue arrow on top

Calling a gRPC Service from a React Client

“Oh! The REST API Response Is Big!”

At adjoe, we have several microservices, some of which communicate with each other using gRPC . There are some possible scenarios where these gRPC services serve many clients, such as web applications. And to communicate with these gRPC services from a web application, we use a REST API server to transfer requests and responses.

diagram of serving a web application by grpc services using a REST API in the middle

In some scenarios, the response can be huge and potentially lead to performance issues – such as operational latency and delay. This is why I was curious to see if we could tackle this issue using gRPC in a web application and removing the REST API server.

diagram of serving a web application by grpc services using grpc-web and protocol buffers

What are the pros and cons of using gRPC in a web application? 

Pros

  • Protocol efficiency: gRPC uses the protocol buffer’s binary serialization format, which is highly efficient and results in smaller message payloads compared to other serialization formats like JSON. This can reduce network bandwidth requirements and improve response times, which can be especially important in web-based applications that may have limited bandwidth or high latency.
  • Language agnostic: gRPC supports multiple programming languages and platforms, including Java, C++, and JavaScript. This can make it easier to build web-based applications that use multiple programming languages.
  • Interoperability: gRPC supports multiple protocols and serialization formats, including HTTP/2, protocol buffers, and JSON. This can make it easier to build web-based applications that interact with other systems or services that use different protocols or serialization formats.

Cons

  • Complexity: gRPC can be more complex than other RPC frameworks – especially, for developers who are new to the framework or unfamiliar with the underlying technologies like protocol buffers or HTTP/2.
  • Learning curve: Developers may need to learn new concepts and technologies when working with gRPC, which can increase the learning curve and time-to-market for web-based applications.
  • Tooling support: While gRPC has good support for several programming languages, the tooling support for gRPC can be more limited than other RPC frameworks.
  • Debugging: Debugging gRPC applications can be more complex than other RPC frameworks – especially, for developers who are new to the framework or unfamiliar with the underlying technologies.

The Simplified gRPC Structure

diagram of simplified gRPC structure

Using a React gRPC Client

To call a gRPC service from a React client, we’ll need to use a gRPC client library for JavaScript. One such library is grpc-web, which is a JavaScript client library for gRPC-web. 

Another thing that we need to pay attention to is that gRPC uses HTTP/2 as a transport protocol, and no native browser support exists at the time of writing this article. This is because gRPC relies on several features of HTTP/2 including multiplexing, header compression, server push and flow control. This means that we cannot directly call a gRPC service from a browser the way we call a web API. To solve this problem, we will use a proxy (Envoy proxy) to convert HTTP/1.1 to HTTP/2 and visa versa. 

This is what calling a gRPC service from a browser using the Envoy proxy looks like.

diagram of using Envoy proxy to call gRPC from browser

Let’s go over the basic steps you need to follow to call a gRPC service from a React client.

  1. Install the grpc-web package (npm install grpc-web)
  2. Define your gRPC service in a .proto file
  3. Use the protoc compiler to generate client stubs for your service
  4. Implement the gRPC backend server 
  5. Configure the Envoy proxy
  6. Write JavaScript client code 
  7. You’re ready to go

What’s the Bigger Picture?

This is what we are aiming to do and how the Envoy proxy is going to help us.

diagram showing how adjoe’s project is going to call gRPC server

In the sample project, these are the main components:

  • Client: a React application created by CRA
  • Server: written in JavaScript
  • A gRPC service definition: written in protocol buffer 
  • Client stubs written in proto-gen-grpc-web.sh
  • Envoy.yaml: the Envoy proxy to convert HTTP/1.1 to HTTP/2 and visa versa 
  • Three Docker files for client, server, and Envoy

Here is the structure of the project that we are going to work on:

project structure grpc web

Let’s say we want to transfer a name to the server and receive a greeting message. To do this, we need to define a request, response, and a service agreement written in protobuf.

// The request message contains the user's name.
message GreetRequest {
  string name = 1;
}
// The response message containing the greetings
message GreetReply {
  string message = 1;
}
// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc Greet(GreetRequest) returns (GreetReply);
}

With the above agreement, both server and client know the structure of the request and response, and there is no need to send the object schema in every request and response – as we have in JSON and XML. This reduces the message size, which helps us in scenarios when a large amount of data needs to be transferred over the network. 

Another feature is that in gRPC, data is converted to binary format, which, again, enhances performance, but it is not human-readable.

Client Code

In order to generate the code for client application, we need to run the following command in the root:

./proto-gen-grpc-web.sh

Now there is a folder in the client/src, which includes all the generated code you need.

screenshot of generated codes

The following code shows how simple it is to use these generated codes and send a greet request in App.tsx.

import { GreeterClient } from "./proto/GreetingServiceClientPb";
import { GreetRequest } from "./proto/greeting_pb";


const greetClient = async (name: string) => {
 const EnvoyURL = "http://localhost:8000";
 const client = new GreeterClient(EnvoyURL);
 const request = new GreetRequest();
 request.setName(name);
 const response = await client.greet(request, {});
 console.log(response);
 const div = document.getElementById("response");
 if (div) div.innerText = response.getMessage();
};

The generated code gives us the strongly typed feature that we do not have in REST.

The following code is the app function component.

function App() {
 const [name, setName] = useState("");
 const onClickGreet = () => {
   if (name) greetClient(name);
 };


 return (
   <div className="App">
     <input
       type="text"
       value={name}
       onChange={(e) => setName(e.target.value)}
     />
     <button onClick={onClickGreet}>greet</button>
     {name && <div id="response"></div>}
   </div>
 );
}

Server Code

Now that we have our client ready, let’s take a quick look at the server.js. Here is the main function on the server side.

if (require.main === module) {
 var server = getServer();
 console.log("Server started");
 server.bindAsync(
   "0.0.0.0:50051",
   grpc.ServerCredentials.createInsecure(),
   (err, port) => {
     assert.ifError(err);
     server.start();
   }
 );
 process.on("SIGTERM", () => {
   server.close(() => {
     console.log("Server terminated");
   });
 });
}

We add our services to the gRPC server via getServer. Here, we add Greeter service.

/**
 * @return {!Object} gRPC server
 */
function getServer() {
  var server = new grpc.Server();
  server.addService(greeting.Greeter.service, {
    greet: doSayHello,
  });
  return server;
}
doSayHello
/**
 * @param {!Object} call
 * @param {function():?} callback
 */
function doSayHello(call, callback) {
  callback(null, { message: "Hello " + call.request.name + "!" });
}

HTTP1.1 to HTTP/2 

In order to make communication between the browser and gRPC server possible, we need a proxy (Envoy proxy) to convert HTTP/1.1 to HTTP/2. This proxy is defined in the envoy.yaml file. 

There are also three Docker files for the client, server, and Envoy – and docker-compose to build and run the project.

See the Output

To run the project, we need to run the following commands.

docker-compose build
docker-compose up

Let’s go to http://localhost:8081/ in the browser, send a request, and get a response. By opening the Network tab, we can see that the Request Payload and Response are in binary format, as mentioned earlier.

screenshot of request payload in gRPC (name=johnDoe)
request payload in gRPC (name=johnDoe)
Screenshot of response in gRPC (message=Hello John Doe!)
response in gRPC (message=Hello John Doe!)
screenshot of output
Output

The complete code can be found here.

Calling a gRPC Service from a Web Application

Using gRPC in web-based applications can provide significant performance benefits and reduce development time, but it can also introduce complexity and a learning curve for us as developers. We need to carefully evaluate the benefits and tradeoffs of using gRPC in our web application and choose the best tool for our specific use case. Now that we learned using gRPC in a web application is possible; it provides performance benefits and complexity. 

We tried to thoroughly assess the pros and cons of gRPC-web before deciding whether to use it in our web-based applications. At adjoe we decided to postpone adding this feature and try to first reduce the response size. We are confident that this experiment was successful and we can consider it in the future, if need be.

Join Our Frontend Developers

Check Our Careers