Build and Deploy a gRPC-Web App Using Rust Tonic and React
Tags: webdev, tutorial, programming, react
Published: 2023-07-19 12:17:28

## Introduction

[gRPC](https://grpc.io/) is a modern, high performance remote procedure call (RPC) framework that can be run in any environment.  Built on [protocol buffers (commonly called protobufs)](https://protobuf.dev/), gRPC is extensible and, efficient, and has wide support in many languages and runtimes.  You can take a look at our [what is gRPC post](https://www.koyeb.com/blog/what-is-grpc) to learn more.

In this tutorial, we will go over how to deploy a React application backed by a Rust-based gRPC API to Koyeb.  The demo application is a movie database website that feature showcases a selection of movies and their associated metadata.  You can find the source for the two application components here:

* [gRPC API written in Rust](https://github.com/filhodanuvem/movies-rust-grpc)
* [React web frontend](https://github.com/filhodanuvem/movies-react)

## Prerequisites

In order to follow along with this tutorial, you need the following:

* A [Koyeb account](https://app.koyeb.com/auth/signup) to deploy the Rust and React services to.  Koyeb's free tier allows you to run two services every month for free.
* The [protobuf compiler](https://github.com/protocolbuffers/protobuf/releases/latest) installed on your local computer.  We will use this to generate the language-specific stubs from our data format.
* [Rust and `cargo`](https://www.rust-lang.org/learn/get-started) installed on your local computer to create the gRPC API service.
* [Node.js and `npm`](https://nodejs.org/en/download) installed on your local computer to create the React-based frontend.

Once you have satisfied the requirements, continue on to get started.

## Create the Rust API

We will start by creating the Rust API for the backend and the protobuf file that defines the data format both of our services will use to communicate.

### Create a new Rust project and install the dependencies

Start by defining a new Rust project.

Use the `cargo` command to generate a new project directory initialized with the expected package files:

```bash
cargo new movies-back
```

Next, move into the new project directory and install the project's dependencies:

```bash
cd movies-back
cargo add tonic@0.9.2 tonic-web@0.9.2
cargo add prost@0.11 prost-types@0.11
cargo add --build tonic-build@0.8
cargo add tonic-health@0.9.2
cargo add tower-http@0.2.3
cargo add --features tokio@1.0/macros,tokio@1.0/rt-multi-thread tokio@1.0
```

By default, web browsers do not support gRPC, but we will use [gRPC-web](https://github.com/grpc/grpc-web) to make it possible.

### Define the data format

Next, create the data format by creating a protobuf definition file.

Create a new directory called `proto`:

```bash
mkdir proto
```

Inside, create a new file named `proto/movie.proto` with the following contents:

```protobuf
syntax = "proto3";
package movie;

message MovieItem {
  int32 id = 1;
  string title = 2;
  int32 year = 3;
  string genre = 4;
  string rating = 5;
  string starRating = 6;
  string runtime = 7;
  string cast = 8;
  string image = 9;
}

message MovieRequest {
}

message MovieResponse {
  repeated MovieItem movies = 1;
}

service Movie {
  rpc GetMovies (MovieRequest) returns (MovieResponse);
}
```

The `proto/movie.proto` file defines our data format using the [protobuf format](https://protobuf.dev/programming-guides/proto3/).  It specifies a data structure to hold all of the data about a movie and outlines what a request and response for that data will look like.  This definition will be used to define the API between our services.

## Create the backend service

Now that we have our data format, we can create our backend service.

Start by configuring the Rust build process to compile the protobuf file. Create a file called `build.rs` with the following content:

```rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
  tonic_build::compile_protos("./proto/movie.proto")?;
  Ok(())
}
```

Now we can implement the actual API server with the `GetMovies` endpoint.  Open the `src/main.rs` file and replace the contents with the following:

```rust
use std::env;
use tonic::{transport::Server, Request, Response, Status};
pub mod grpc_movie {
    tonic::include_proto!("movie");
}
use grpc_movie::movie_server::{Movie, MovieServer};
use grpc_movie::{MovieRequest, MovieResponse};

#[derive(Debug, Default)]
pub struct MovieService {}

#[tonic::async_trait]
impl Movie for MovieService {
    async fn get_movies(
        &self,
        request: Request<MovieRequest>,
    ) -> Result<Response<MovieResponse>, Status> {
        println!("Got a request: {:?}", request);

        let mut movies = Vec::new();
        movies.push(grpc_movie::MovieItem {
            id: 1,
            title: "Matrix".to_string(),
            year: 1999,
            genre: "Sci-Fi".to_string(),
            rating: "8.7".to_string(),
            star_rating: "4.8".to_string(),
            runtime: "136".to_string(),
            cast: "Keanu Reeves, Laurence Fishburne".to_string(),
            image: "http://image.tmdb.org/t/p/w500//aOIuZAjPaRIE6CMzbazvcHuHXDc.jpg".to_string(),
        });
        movies.push(grpc_movie::MovieItem {
            id: 2,
            title: "Spider-Man: Across the Spider-Verse".to_string(),
            year: 2023,
            genre: "Animation".to_string(),
            rating: "9.7".to_string(),
            star_rating: "4.9".to_string(),
            runtime: "136".to_string(),
            cast: "Donald Glover".to_string(),
            image: "http://image.tmdb.org/t/p/w500//8Vt6mWEReuy4Of61Lnj5Xj704m8.jpg".to_string(),
        });
        movies.push(grpc_movie::MovieItem {
            id: 3,
            title: "Her".to_string(),
            year: 2013,
            genre: "Drama".to_string(),
            rating: "8.7".to_string(),
            star_rating: "4.1".to_string(),
            runtime: "136".to_string(),
            cast: "Joaquin Phoenix".to_string(),
            image: "http://image.tmdb.org/t/p/w500//eCOtqtfvn7mxGl6nfmq4b1exJRc.jpg".to_string(),
        });

        let reply = grpc_movie::MovieResponse { movies: movies };

        Ok(Response::new(reply))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let port = env::var("PORT").unwrap_or("50051".to_string());
    let addr = format!("0.0.0.0:{}", port).parse()?;
    let movie = MovieService::default();
    let movie = MovieServer::new(movie);
    let movie = tonic_web::enable(movie);

    let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
    health_reporter
        .set_serving::<MovieServer<MovieService>>()
        .await;

    println!("Running on port {}...", port);
    Server::builder()
        .accept_http1(true)
        .add_service(health_service)
        .add_service(movie)
        .serve(addr)
        .await?;

    Ok(())
}
```

The application uses the [tonic gRPC implementation](https://docs.rs/tonic/latest/tonic/) to build and serve the API for our backend based on the structures and interfaces defined by the protobuf file.  In a real world scenario, this would typically be backed by a database containing the movie data, but to simplify the implementation, we just include data for a few movies inline.

The service will run on port 50051 by default (modifiable with the `PORT` environment variable) and will respond for requests for movies using with response objects as defined by the protobuf file.

### Test the API backend

The API backend is now complete, so we can start the server locally to test its functionality by typing:

```bash
cargo run
```

The API server will be built and start running on port 50051.  You can test the functionality using a gRPC client of your choice like [`grpcurl`](https://github.com/fullstorydev/grpcurl) or [Postman](https://postman.com/).

For example, you can request the list of movies using `grpcurl` by typing the following in your project's root directory with the Rust service running:

```bash
grpcurl -plaintext -import-path proto -proto movie.proto 127.0.0.1:50051 movie.Movie/GetMovies
```

The service should return the list of movies as expected:

```json
{
  "movies": [
    {
      "id": 1,
      "title": "Matrix",
      "year": 1999,
      "genre": "Sci-Fi",
      "rating": "8.7",
      "starRating": "4.8",
      "runtime": "136",
      "cast": "Keanu Reeves, Laurence Fishburne",
      "image": "http://image.tmdb.org/t/p/w500//aOIuZAjPaRIE6CMzbazvcHuHXDc.jpg"
    },
    {
      "id": 2,
      "title": "Spider-Man: Across the Spider-Verse",
      "year": 2023,
      "genre": "Animation",
      "rating": "9.7",
      "starRating": "4.9",
      "runtime": "136",
      "cast": "Donald Glover",
      "image": "http://image.tmdb.org/t/p/w500//8Vt6mWEReuy4Of61Lnj5Xj704m8.jpg"
    },
    {
      "id": 3,
      "title": "Her",
      "year": 2013,
      "genre": "Drama",
      "rating": "8.7",
      "starRating": "4.1",
      "runtime": "136",
      "cast": "Joaquin Phoenix",
      "image": "http://image.tmdb.org/t/p/w500//eCOtqtfvn7mxGl6nfmq4b1exJRc.jpg"
    }
  ]
}
```

### Create a Dockerfile

When we deploy the backend to Koyeb, the application will be run in a container.  We can define a `Dockerfile` for our project to describe how the project's code should be packaged and run.

In the project's root directory, create a new file called `Dockerfile` with the following content:

```dockerfile
FROM rust:1.64.0-buster as builder

# install protobuf
RUN apt-get update && apt-get install -y protobuf-compiler libprotobuf-dev

COPY Cargo.toml build.rs /usr/src/app/
COPY src /usr/src/app/src/
COPY proto /usr/src/app/proto/
WORKDIR /usr/src/app
RUN rustup target add x86_64-unknown-linux-musl
RUN cargo build --target x86_64-unknown-linux-musl --release --bin movies-back

FROM gcr.io/distroless/static-debian11 as runner

# get binary
COPY --from=builder /usr/src/app/target/x86_64-unknown-linux-musl/release/movies-back /

# set run env
EXPOSE 50051

# run it
CMD ["/movies-back"]
```

When you are finished, add all of the files for the API to a new GitHub repository so that we can deploy it to production later.

We are ready now to create the react application that will consume the API.

## Create the React application

Now that the backend is complete, we can begin working on the React frontend service for our application.

### Generate a new React project

The fastest way to create a basic react application is with the `create-react-app`.  Check to make sure you are not in the Rust service's directory and then type:

```bash
npx create-react-app movies-front
```

This will a new project directory for your frontend and generate some basic files to help you get started.

Move into the new directory and start the service to validate that everything installed correctly:

```bash
cd movies-front
npm run start
```

A development server will open on port 3000 and React will attempt to open a new browser window to view it.  You can visit `localhost:3000` if you are not automatically directed there.  The default React development page should appear:

![The default landing page for a new React project](https://www.koyeb.com/static/images/tutorials/grpc-multi-tier/react_default_landing_page.png)

Press <kbd>Ctrl</kbd>-<kbd>c</kbd> when you are finished to stop the development server.

### Configure the Tailwind CSS framework

Our react application will show a list of movies on a page.  To speed up the process of styling it, we are going to use [Tailwind CSS](https://tailwindcss.com/).  Install the necessary packages and initialize Tailwind by typing:

```bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

Now that Tailwind is installed, we need to configure the React application to support it.

First, open the `tailwind.config.js` file.  Modify the `content` property to pick up all of our expected CSS content:

```javascript
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

We will be using Sass instead of CSS directly, so we need to install the `sass` package next:

```bash
npm install sass
```

Remove the `src/App.css` file and replace it with an `src/App.scss`.  Inside, we import all of the Tailwind code that we require:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

We can test Tailwind by changing the main React application file.  Change the contents of the `src/App.js` file like this:

```javascript
import './App.scss';
import Movie from './Movie';

function App() {
  // for now we will
  let movies = [
    {
      id: 1,
      title: 'The spiderman across the spider verse',
      year: 2023,
      genre: 'animation',
      rating: 'L',
      starRating: '4.9',
      runtime: '2h 22min',
      cast: 'Donald Glover',
      image: 'http://image.tmdb.org/t/p/w500//8Vt6mWEReuy4Of61Lnj5Xj704m8.jpg'
    }
  ]

  return (
    <div className="App">
      {movies.map((movie) => (
          <Movie key={movie.id} details={movie} />
      ))}

    </div>
  );
}

export default App;
```

Here, we create an application that will serve our movie list when the page is requested.  At this point, we just mock up a single movie so that we can test our CSS styling.

Create a `src/Movie.js` file to define how the movie should be styled and displayed:

```javascript
export default function Movie(props) {
  let movie = props.details

  return (
    <article className="flex items-start space-x-6 p-6">
      <img src={movie.image} alt="" width="60" height="88" className="flex-none rounded-md bg-slate-100" />
      <div className="min-w-0 relative flex-auto">
        <h2 className="font-semibold text-slate-900 truncate pr-20">{movie.title}</h2>
        <dl className="mt-2 flex flex-wrap text-sm leading-6 font-medium">
          <div className="absolute top-0 right-0 flex items-center space-x-1">
            <dt className="text-sky-500">
              <span className="sr-only">Star rating</span>
              <svg width="16" height="20" fill="currentColor">
                <path d="M7.05 3.691c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.372 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.539 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.783.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.363-1.118L.98 9.483c-.784-.57-.381-1.81.587-1.81H5.03a1 1 0 00.95-.69L7.05 3.69z" />
              </svg>
            </dt>
            <dd>{movie.starRating}</dd>
          </div>
          <div>
            <dt className="sr-only">Rating</dt>
            <dd className="px-1.5 ring-1 ring-slate-200 rounded">{movie.rating}</dd>
          </div>
          <div className="ml-2">
            <dt className="sr-only">Year</dt>
            <dd>{movie.year}</dd>
          </div>
          <div>
            <dt className="sr-only">Genre</dt>
            <dd className="flex items-center">
              <svg width="2" height="2" fill="currentColor" className="mx-2 text-slate-300" aria-hidden="true">
                <circle cx="1" cy="1" r="1" />
              </svg>
              {movie.genre}
            </dd>
          </div>
          <div>
            <dt className="sr-only">Runtime</dt>
            <dd className="flex items-center">
              <svg width="2" height="2" fill="currentColor" className="mx-2 text-slate-300" aria-hidden="true">
                <circle cx="1" cy="1" r="1" />
              </svg>
              {movie.runtime}
            </dd>
          </div>
          <div className="flex-none w-full mt-2 font-normal">
            <dt className="sr-only">Cast</dt>
            <dd className="text-slate-400">{movie.cast}</dd>
          </div>
        </dl>
      </div>
    </article>
  )
}
```

If you start up the development server again, you should be able to see the movie mocked out:

```bash
npm run start
```

![Single movie mock display](https://www.koyeb.com/static/images/tutorials/grpc-multi-tier/dummy_movie_display.png)

### Fetch movie lists from the API

Next, instead of displaying a hardcoded movie, we will modify the application to fetch data from the gRPC API.

First, install protobuf and gRPC web support for React:

```bash
npm install google-protobuf@~3.21.2 grpc-web@~1.4.1
```

Next, we need to generate the movie entity in JavaScript based on the same protobuf file we defined for the backend Rust application.  To do this, we will use the `protoc` command included in the protobuf installation from the prerequisites.

Assuming the `movies-back` and `movies-front` are located next to each other, you can generate the appropriate JavaScript files with the protobuf definition by running the following command in the `movies-front` directory:

```bash
# change the proto_path to the correct place where movie.proto is.
protoc --proto_path=../movies-back/proto/ movie.proto --grpc-web_out=import_style=commonjs,mode=grpcweb:src --js_out=import_style=commonjs,binary:src
```

This will generate the appropriate JavaScript stubs from your protobuf file so that the React application understands how to communicate with the API.

Now we can change the `src/App.js` file to use gRPC instead of serving a hardcoded movie:

```javascript
import './App.scss';
import Movie from './Movie';
import { useState, useEffect} from 'react';

const proto = {};
proto.movie = require('./movie_grpc_web_pb.js');

function App() {
  let url  = process.env.REACT_APP_BACKEND_URL;
  if(url == null){
    url = "http://localhost:50051"
  }

  const client = new proto.movie.MovieClient(url, null, null);
  let [movies, setMovies] = useState([])
  useEffect(() => {
    const req = new proto.movie.MovieRequest();
    client.getMovies(req, {}, (err, response) => {
      if(response == null){
        return
      }

      if (response.getMoviesList().length === 0) {
        return
      }

      let m = []
      response.getMoviesList().forEach((movie) => {
        console.log(movie.toObject())
        m.push(movie.toObject())
      })
      setMovies(m)
    })
  }, [])

  return (
    <div className="App">
      {movies.map((movie) => (
          <Movie key={movie.id} details={movie} />
      ))}
    </div>
  );
}

export default App;
```

Start the development server again to test the new changes:

```bash
npm run start
```

This time, you should see the full movie list as served from the API backend:

![Full movie display from backend API](https://www.koyeb.com/static/images/tutorials/grpc-multi-tier/full_movie_display.png)

After confirming that the application works as expected, add the React app's files to a new GitHub repository and push them.  We will deploy the application to production in the next stage.

## Deploy the application to Koyeb

Now that our frontend and backend are working as expected, we can deploy both services to Koyeb.

### Deploy the API service

First, we will deploy the Rust application to Koyeb.

In the [Koyeb control panel](https://app.koyeb.com/), click the **Create App** button to get started.  Select GitHub as your deployment method and then choose the repository for the backend API from your repository list.  Alternatively, you can use the example repo for this service which contains the same code we've discussed by putting `https://github.com/filhodanuvem/movies-rust-grpc` in the **Public GitHub repository** field.

On the next page, choose **Dockerfile** as the builder:

![Koyeb API deployment configuration](https://www.koyeb.com/static/images/tutorials/grpc-multi-tier/koyeb_api_deploy.png)

Click **Advanced** to expand additional settings.  Change the value of the `PORT` environment variable to 50051.  In the **Exposing your service** section, modify the port to 50051 as well.  Select `HTTP/2` from the protocol drop down list and set the path to `/api`.

![Koyeb API port and health check configuration](https://www.koyeb.com/static/images/tutorials/grpc-multi-tier/koyeb_api_port_config.png)

When you are finished, click the **Deploy** button to deploy the API backend.

On the API's service page, copy the value of the **Public URL**.  We will need this value when configuring our React application.

## Deploy the React application

Now that the API is running we can deploy the React application.  Unlike the API, we will use the buildpack builder for this project rather than building from a Dockerfile.

In the [Koyeb control panel](https://app.koyeb.com/), click on the application that includes the API service you just deployed.  From the service's index page, click the **Create Service** button to deploy an additional service within the context of the same Koyeb App.

On the following screen, select **GitHub** as the deployment method and click the repository for the frontend React application from your repository list.  Alternatively, you can use the example repo for this service which contains the same code we've discussed by putting `https://github.com/filhodanuvem/movies-react` in the **Public GitHub repository** field.

On the next page, select **Buildpack** as the builder:

![Koyeb frontend deployment configuration](https://www.koyeb.com/static/images/tutorials/grpc-multi-tier/koyeb_frontend_deploy.png)

Click **Advanced** to expand additional settings.  Click the **Add Variable** button and create a new variable called `REACT_APP_BACKEND_URL`.  Use the API service's public URL that you copied as the value.  It should look something like the following:

```bash
REACT_APP_BACKEND_URL=https://<YOUR_APP_NAME>-<KOYEB_ORG_NAME>.koyeb.app/api
```

![Koyeb frontend environment variable configuration](https://www.koyeb.com/static/images/tutorials/grpc-multi-tier/koyeb_frontend_env_vars.png)

When you are finished, click the **Deploy** button to begin building and deploying your application.  When the deployment is complete, use click the React application's public URL to access the movie database site.

## Conclusion

In this guide, we created and deployed an end-to-end application composed of a Rust backend and a React frontend.  The two services communicate using gRPC.  We deployed both services to Koyeb to take advantage of its native gRPC support.  The services are able to communicate securely in order to fulfill user requests for the movie database site.

If you have any questions or suggestions, feel free to reach out on our [community Slack](https://slack.koyeb.com/).