How to build a website using Rust, gRPC-web, React by Christopher Scholz | October, 2022

A guide to building your own internet home

Unsplash. Photo by Carlos Muza on

This is how I implemented a small client-server app that serves my personal website using a gRPC-web interface and what I learned while doing it. The client serves a static page and receives the content via gRPC. The content is structured like Editor.js.

For the server, I am using Rust and Tonic framework. Since gRPC is based on http/2, the Envoy proxy is often used to convert http/1.1 to http/2. For this app, I took a different route and used tonic-web crate.

For the client, I am using React and serving it through Express.

setup environment

To build the app, we need to install Rust and Protocol Buffers Compiler (Protoc).

setup package

After setting up our development environment, we can start working on our project. Let’s first start a new project directory with the following command:

mkdir grpc_app
cd grpc_app

Using the Rust Package Manager, we can now create a package.

cargo new server
cd server

For the app interface, we need the following:

UUID crate is used to create uuids v4. This is only needed for the app content, but not for the general implementation of the interface.

Let’s add them to our server app, as shown below:

cargo add tonic@0.8 tonic-web@0.4
cargo add prost@0.11 prost-types@0.11
cargo add --features tokio@1.0/macros,tokio@1.0/rt-multi-thread tokio@1.0
cargo add --features uuid@1.2/v4 uuid@1.2
cargo add --build tonic-build@0.8

This should result in a Cargo.tomlhence:

create protocol buffer definition

As mentioned, we want to build our interface based on the Editor.js specification. We will implement block types Paragraph, Header And List,

Let’s first create a new file in the proto directory:

mkdir proto
cd proto
touch page.proto

First, we need to define the syntax and give the syntax a name:

Next, we define our service. The service consists of only one call, receive and return messages.

We’ll need to implement those two messages. PageRequest is just a string, but PageReply We will have a response like our structured Editor.js.

In form of PageReply is a timestamp, we will need to import or create an additional type. We will import it from well-known types of Google.

we then define ourselves PageReply, To create a list, we use the feature repeated, The block can then be a paragraph, a header, and a list. For this we use the feature oneof, a oneof Cannot be done repeated, So we need to create an intermediate message named Block,

Now we can implement different block types. for ListBlock We will need a constraint for the style field. This can be achieved through a enum,

Full code can be found here.

generate proto stub

To generate the proto stub, we need to add a build script (build.rs) in the main directory of our app.

apply service

To write our app, we edit src/main.rs file that was created by cargo new server,

First, we will declare the usage of some modules and publish the generated proto page stub.

Next, we declare our structure and define our page implementation. We’ll come to the argument later.

To run our app, we need to add our main function. we use tokio::main Macro to help us with this. Here we also use tonic_web To provide our service on http/1.1 please crate and define CORS headers.

The logic of our app is based on input. We will need to define three cases inside ourselves get_page Do as follows:

  • House
  • impression
  • a default case

Each match arm is then created by creating our blocks, for example, like this:

Full code can be found here.

run service

Now we can run the app with this command:

cargo run

Since we haven’t implemented the client yet, we test our server through BloomRPC. For this we can import our page.proto, then set our server to 127.0.01:8000 And switch from gRPC to web. We will get our response when we hit the green play button.

dockerize

Every good app needs to be dockerized. We do this by first building it and then copying the binary into a distroless image.

let’s make ours Dockerfile

As we will also create a Docker image for our client later, let’s set up a docker-compose.json In our project main directory.

Then we can run our Docker image and test again via BloomRPC.

docker-compose up --build

setup environment

Install node.

In addition to the Protocol Buffers compiler installed for the server, we also need two plugins that generate the JavaScript proto stub. Unfortunately, the JavaScript implementation of protobuf-javascript is not well maintained. At the time of writing this, the release binaries were not working, so we need to compile it from source.

First, we download the proto-gen-grpc-web binary from https://github.com/grpc/grpc-web/releases/tag/1.4.1 and place it next to our protoc binary. Be sure to rename the binary protoc-gen-grpc-web,

Second, clone the protobuf-javascript repository and reset it to the specified commit.

git clone <https://github.com/protocolbuffers/protobuf-javascript> /home/protobuf-javascript
git reset --hard 3ff6090f139d71453062fb96c66e9aff801709c2

We will need to install Bezel: https://docs.bazel.build/versions/main/install.html

Then we can load the dependencies and build the binary

cd protobuf-javascript
npm install
npm run build

the last thing is to copy bazel-bin/generator/protoc-gen-js On the same path as the protoc binary.

setup package

Now we can build React app by running the following:

npx create-react-app client
cd client

In addition to React dependencies, we also need to add react-router, grpc-web and google-protobuf modules.

npm install --save google-protobuf@~3.21.2 grpc-web@~1.4.1 react-router-dom@~6.4.2

generate proto stub

We need to create our own proto-javascript stub to encode/decode serialize/deserialize gRPC data. For this we can use protoc binary. copy page.proto called in a new directory in the client app protoThen run the following:

protoc --proto_path=proto page.proto \\
--grpc-web_out=import_style=commonjs,mode=grpcweb:src \\
--js_out=import_style=commonjs,binary:src

This will generate two javascript files called page_grpc_web_pb.js And page_pb.js inside the src directory. With large proto files, this will generate quite a bit of code. Instead of generating this static code, you can use a node module like protobuf.js.

mode is set to grpcweb, Instead, we could have set it to grpcwebtext, More about the mod here.

To support the binary proto format, we added the binary import_style To js_out, More about the options here.

To make sure this stub is generated every time, we run npm install and add another script in our package.json,

implement client

Our client will be fairly straightforward.

First, we need a public/index.html

Then we will implement our React app src/index.js,

First, import all required modules and define React Routes.

inside us React.StrictModeWe will add React Router with three routes

  • / → Page component with property Page=Home
  • /impressum → page component page with property = impressionsum
  • Other → NoPage Components

NoPage Component is just some HTML without any arguments

for Page component, we need to first import our generated JavaScript stub and create a PageClient Thing.

Then we can define our component without arguments.

For the sake of argument, we can use a . will build PageRequest From page property, call getPage and set the position of the component with the generated HTML PageReply,

We use React to generate our HTML. Since the response contains a list of blocks, we can iterate over them and return the corresponding HTML.

For my implementation, I’ve added some styling to it and some other static HTML bits and pieces.

run client

In order to run our client through the development server, we need to run the React Start script. It will also open a new browser window and show the website.

npm start

serve via express

The development server is not the only way to deploy a React app. We can read about some possibilities here: https://create-react-app.dev/docs/deployment/

We choose to deploy the app via Express. To do this, we create a new nodejs app inside our client app and add express dependencies.

npm init express
cd express
npm install --save express@~4.18.2

then we need a index.js File to serve React app. Here’s what it looks like:

This won’t work, because we haven’t built a production build yet. We can do this by running the following command in the client directory:

npm run build

Then we can run express server using this code:

cd express
node index.js

dockerize

Like the server app, we dockerize the client.

This will be a bit more complicated as we need to make the following:

the creator

  • Install protobuf compiler and two generator plugins
  • Build Our React App
  • install express

Runner

  • Add node users and groups
  • install nodejs and dum-init
  • Copy relevant files from builder

Now, add our client to docker-compose.json In our project main directory.

After putting everything together, we both run and browse the images http://127.0.0.1:8000,

docker-compose up --build

I had a lot of trouble getting the Protocol Buffers Compiler JavaScript plugins to work. But afterwards, setting everything up was easy enough. The gRPC-web communication with the server worked without any problems and used only about 2/3 the size of the corresponding JSON.

But encoding/decoding and serializing/deserializing of messages takes quite a bit of code, which seems overly complicated for a webapp. protobuf.js might be a way to do this, but it would also require some code to generate the stubs on the fly.

I like how easy it is to understand the structure of the data using proto files. These proto files act as a contract between the client and the server.

I miss the possibility of writing a query when using GraphQL because it looks like a mostly binary REST-like interface. It would certainly be possible to create a protocol buffer like GraphQL, but it would be quite a bit of work. For now, I’ll still keep using GraphQL for web client-to-server communication.

The full code for the application can be found here.

Leave a Comment