Ditching REST with gRPC-web and Envoy
Ever since gRPC was introduced it became really popular among API developer community. The reason gRPC became so popular was its support for polyglot implementation (server and client can be written in separate languages), its focus on the performance from get go (gRPC is based on HTTP/2) and a great tool set (gRPC use protobuf for message and service descriptions and clients can be generated automatically without writing single line of code).
Till some years back, this goodness was limited to mobiles and servers and was not available to front end developers and they still had to use REST interfaces. This was due to the HTTP/2 limitations in the browsers. For example
- A request in browser can’t be forced to be HTTP/2.
- HTTP/2 frames are not directly available to the libraries.
In comes grpc-web, an effort by Google and Improbable to implement a gRPC spec for browsers. This spec use HTTP/1.x with a gateway proxy such as Envoy (which transparently translates HTTP/1.x to HTTP/2 that is expected by gRPC server).
gRPC-web only supports Unary and Server-side streaming in grpcwebtext
mode (we’ll cover what this means in subsequent sections)
In this post we will implement a gRPC server in golang and implement the client in JavaScript using gRPC-web. We will see examples of both unary and server-side streaming API calls.
So lets get started.
Table of Content
- Prerequisites
- Protobuf definitions
- Implementing Unary server API
- Consuming Unary API
- Fixing communication errors with Envoy
- Implementing Server-side streaming
- Conclusion
Prerequisites
We need to install few tools to successfully plough through this post.
VS Code
or code editor of your choice to write our codes.- Golang compiler and tools.
- Python3 — We’ll use python’s HTTP server to serve our client.
- Node (npm comes pre-bundled with node) and npx tools.
- Docker — We’ll be running Envoy proxy as a docker container.
protoc
the protobuf compiler.- Golang gRPC package and protobuf protoc plugin.
- gRPC-web’s
protoc-gen-grpc-web
plugin.
Follow the installation instructions for each individual tool. I’m skipping the instructions here for the sake of brevity but do let me know if you face any errors, I’ll be happy to help.
This post is by no means an introduction post for any of the languages or frameworks used here. I’ll be linking introductory material for each of them for you to get up-to speed.
Protobuf definitions
We’ll start by creating the protobuf definition for our service. Create a calculator.proto
file and place it in protos
folder. For now, we’ll just add Unary service definition and evolve it to add Server-side streaming in the 2nd part.
grpc-web
├── protos
│ └── calculator.proto
├── server
└── client
We have added two messages namely AddRequest
that takes up two numbers to add, AddResponse
that returned the result of addition and a calculator service with one rpc call Add
which takes in an AddRequest
and returns an AddResponse
.
Implementing Unary server API
We’ll compile calculator.proto
file to go code with help of protoc and go protobuf plugin.
Create a calculatorpb
folder inside the server folder and execute following command.
protoc calculator.proto --go_out=plugins=grpc:../server/calculatorpb/
This will generate a calculator.pb.go
file and your directory structure will look like this.
grpc-web
├── protos
│ └── calculator.proto
├── server
│ └── calculatorpb
│ └── calculator.pb.go
└── client
Now initialize a go module by executing following command in the server
folder.
go mod init github.com/kaysush/grpc-calculator
This will add a go.mod
file to your server
folder. Creating a module out of our server
folder will make sure we can access the package in calculator.pb.go
file in our server.go
file which we’ll add in next step.
Lets implement the server now.
grpc-web
├── protos
│ └── calculator.proto
├── server
│ ├── calculatorpb
│ │ └── calculator.pb.go
│ ├── go.mod
│ └── server.go
└── client
Run the server.go
go run server.go
You should see the Starting Calculator server
message on your screen.
The gRPC server is now ready to serve the requests on port 50551
.
Consuming Unary API
Now that our server is running, lets implement our client using gRPC-web
.
Compile the calculator.proto
file for JavaScript using proto-gen-grpc-web
plugin.
protoc calculator.proto --js_out=import_style=commonjs,binary:../client --grpc-web_out=import_style=commonjs,mode=grpcwebtext:../client
This will generate calculator_pb.js
(which has message definitions) and calculator_grpc_web_pb.js
(which has our client implementation).
grpc-web
├── protos
│ └── calculator.proto
├── server
│ ├── calculatorpb
│ │ └── calculator.pb.go
│ ├── go.mod
│ └── server.go
└── client
├── calculator_grpc_web_pb.js
└── calculator_pb.js
Add a package.json
file to client
folder with following content.
{
"name": "grpc-calculator",
"version": "0.1.0",
"description": "gRPC-Web Calculator",
"devDependencies": {
"@grpc/proto-loader": "^0.3.0",
"google-protobuf": "^3.6.1",
"grpc": "^1.15.0",
"grpc-web": "^1.0.0",
"webpack": "^4.16.5",
"webpack-cli": "^3.1.0"
}
}
Add a client.js
file which has our client implementation connecting to our gRPC server.
Remember how I told you that you don’t need to write any code to implement the client with gRPC.
Now let us package our JavaScript code into a minified file.
npm install
npx webpack client.js
This should download all of our dependencies mentioned in package.json
file and then package client.js
into dist/main.js
with all the dependencies.
Create an index.html file which reference our dist/main.js
file and makes the gRPC call.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Calculator - gRPC-Web</title>
<script src="dist/main.js"></script>
</head><body>
</body>
</html>
Your directory structure should look like this now.
grpc-web
├── protos
│ └── calculator.proto
├── server
│ ├── calculatorpb
│ │ └── calculator.pb.go
│ ├── go.mod
│ └── server.go
└── client
├── dist
│ └── main.js
├── calculator_grpc_web_pb.js
├── calculator_pb.js
├── client.js
├── package.json
└── index.html
From client directory start a python
server to start serving our files.
python3 -m http.server 8081 &
Now go to http://localhost:8081 and open the console.
You should see error in the console. You’ll get one or all of following errors.
- Cross Origin Request Blocked
- ERR_CONNECTION_REFUSED
There are two issues with our current server and client implementation.
- Our server does not allow CORS requests and
- Even if it did support CORS,
gRPC-web
do not support HTTP/2 hence we need a gateway proxy likeEnvoy
to translate our HTTP/1.x requests originating from gRPC-web for our gRPC server.
Fixing communication errors with Envoy
As discussed in previous section, we need to put Envoy between our client and server for transparent translation between HTTP/1.x and HTTP/2.
We’ll be using docker to run our envoy proxy. We’ll be using standard envoy.yaml from gRPC-web hello world demo (of course with ports changed as per our implementation)
Create the docker file.
FROM envoyproxy/envoy:latest
COPY ./envoy.yaml /etc/envoy/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml
Create a docker image.
docker build -t my-envoy:1.0 .
Now run the envoy docker container.
docker run -d -p 8080:8080 -p 9901:9901 --network=host my-envoy:1.0
Once envoy is running we need to change port in our client.js
. Now instead of connecting to gRPC server directly on 50551
we’ll be connecting to envoy on 8080
.
Re-package the client.js
again.
npx webpack client.js
Re-load the browser at http://localhost:8081 and open the console.
You should now see your output.
Implementing Server-side streaming
Edit the calculator.proto
file and add a server-side streaming rpc call to generate Fibonacci numbers.
protoc calculator.proto --go_out=plugins=grpc:../server/calculatorpb/Re-compile our proto files for both go and js.protoc calculator.proto --js_out=import_style=commonjs,binary:../client --grpc-web_out=import_style=commonjs,mode=grpcwebtext:../client
Add server implementation in server.go
.
Add the streaming call in our client.js
.
Re package the client.
npx webpack client.js
Re-load the browser and you should see the fibonacci numbers streaming to your client.
Conclusion
gRPC-web is a really mature spec for connecting to exiting gRPC APIs directly from browsers without any REST interface in between. With microservices increasingly using gRPC , gRPC-web fits really nicely into a REST free API world.
In case you find any error in my code or have any question in general, feel free to drop a comment.
Till then Happy Coding! :)