Engineering
February 1, 2023
20 min read

Building gRPC APIs with Rust

Shane Utt
Shane Utt
Staff Software Engineer
Viktor Gamov also contributed to this post.

In recent decades it's become common for communication between backend services to employ HTTP APIs with JSON payloads. Many HTTP APIs adhere (or at least aspire) to REST Principles, though many fall into a category we'll call "REST-like". Some "REST-like" APIs ultimately end up operating like Remote Procedure Call (RPC) APIs in that they are less concerned with CRUD Operations and operate more as if they're calling general procedures on the API endpoint.

RPC APIs can be a great alternative to "REST-like" APIs and operate as a set of functions/subroutines which can be called over a network. RPC APIs are often more lightweight and performant than HTTP APIs, but can also be a little more burdensome to set up initially. The gRPC project was introduced to improve the set-up and tooling experience for creating and maintaining RPC APIs, providing a "batteries included" experience.

gRPC is a modern, open source high performance RPC framework which is a Cloud Native Compute Foundation (CNCF) project that operates on top of HTTP/2 and provides automatic code generation. gRPC can be a helpful solution for your projects to enable quick setup, lighter data transfers and better maintenance costs. Utilizing Protocol Buffers to define services with callable methods, code for servers and clients can be automatically generated in a number of programming languages which reduces the time it takes to build API clients and to provide updates and improvements to them over time.

The Rust Programming Language is a great compliment to our gRPC story so far as we're already thinking about performance. Rust provides excellent execution performance as well as state-of-the-art memory safety guarantees for developing your applications. Rust is not yet considered one of the core languages supported by the gRPC project, but in this walkthrough we will demonstrate how developers can get started building gRPC services in Rust today using the highly capable Tonic framework. We will demonstrate setting up a new project, creating our services with Protocol Buffers, generating client and server code and then ultimately using the API to call methods over the network and stream data.

Automated API Creation Mastered: Self-Service APIs with Konnect for Agility

Demo

Prerequisites

Ensure that you have Rust installed on your system and select an editor of your choice for creating the code.

Note: This demo expects some familiarity with Rust, though all the code is provided so in theory this could be helpful for newcomers. If you're brand new to Rust however, we would recommend checking out the excellent official Learn Rust documentation before continuing, in order to get your bearings.

Note: Tonic requires Rust v1.60.x+. This demo was originally written using v1.65.0, so if you run into trouble with other releases, you might consider giving that specific version a try.

Install the Protocol Buffers Compiler for your system, as this will be needed to generate our server and client code.

Note: This demo was built and tested on an Arch Linux system, but should work on any platform where Tonic and protoc are supported. If you end up having any trouble building Tonic on your system, check out the getting help documentation and reach out to the community.

Download and install Insomnia from official-website. Insomnia is an open source API testing tool, and it supports testing REST, gRPC and GraphQL services.

Step 1: Scaffolding

Choose a directory where you'll be adding code, and generate a new crate for the code with:

$ cargo new --bin demo

Next we'll define protobuf files which will generate client code for us.

Step 2: Service Definition Protobuf

For this demo we'll be creating a service which is responsible for keeping track of the inventory of a grocery store, with the ability to view and create items as well as the ability to watch for changes in inventory (so that we can try a streaming call as well as non-streaming calls).

We will build our grocery store service using Protocol Buffers, which are configured with .proto files wherein the services and types are defined. Start by creating the proto/ directory where our .proto files will live:

Then create proto/store.proto:

The above defines which version of protocol buffers we'll be using, and the package name. Next we'll add our service:

This service provides that calls we can use for a basic level of control over our store's inventory, including creating and removing items, updating their stock quantity/price and viewing or streaming item information. Next we'll create the messages which are the types needed for these calls:

Now we have the service and messages we need to ask the protoc compiler to generate some of our server and our client code.

Step 3: Compiling Protobuf

Now that we have our service, calls and messages all defined we should be able to compile that into a Rust API server and client.

To do so, we'll need to add some dependencies on tonic and prost to handle gRPC and protobufs. Update the Cargo.toml to include them:

Once the dependencies are updated, we'll need to add build tooling that will hook the cargo build step to compile our .proto file during every build. We can do that by creating build.rs:

Note: the --experimental_allow_proto3_optional argument isn't strictly necessary on newer systems with protoc version 3.21.x+, but it won't hurt anything either. This is particularly helpful for users of Ubuntu LTS or other systems where the packaged protoc is significantly older.

We've indicated that we want both client and server built, and the output directory for the generated code should be the src/ directory. Now we should be able to run:

There should now be a src/store.rs created for us with our client and server code conveniently generated.

Step 4: Implementing the Server

Now that we've generated the code for our service, we'll need to add our implementation of the server methods for the client to call.

Start by creating a new src/server.rs file and we'll begin with the imports we'll need:

We'll also provide some helpful error messages for a variety of failure conditions which our API can reach related to inventory management:

Next up we're going to implement the Inventory trait which was generated for us from the proto/store.proto file in the last step. For each of the methods we added to our Inventory service, we'll write our own implementation.

We'll create a StoreInventory object to implement our inventory service:

Note: you may notice we're using lots of async Rust terminology, and imports from the Tokio Runtime. If you're newer to Rust, and the use of these things are a bit confusing, don't worry! There's some great material out there to get you caught up on async Rust: check out the Rust Async Book and the Tokio Runtime Tutorial first and get your bearings.

Our StoreInventory will have an inventory field which contains a threadsafe hashmap, which will be the in-memory storage for our inventory system. We implement the Default trait for convenience, and then we provide the impl Inventory block. Now we can start adding our method implementations for add, remove, update_price and so forth, so the following code blocks should be placed nested inside that impl Inventory for StoreInventory block.

Let's start with adding the add method:

In the above you will find that a Request<Item> is provided (from our client when called) which includes the entire item that we need to store in the inventory. Some validation is performed to ensure data-integrity, we lock the Mutex on our HashMap to ensure thread safety and integrity and then ultimately the item is stored by SKU into the HashMap.

We'll add the remove counterpart as well, which is more simple:

Note: this method returns success for removal of items that didn't exist, but informs the user of that circumstance.

Now that items can be added and removed, they also need to be retrieveable, let's add our get implementation:

The get implementation is small and simple, validating input and returning the inventory Item if present.

We can add and retrieve, but we also need to be able to update in place. Let's add our update_quantity implementation:

Again we provide some validation, and enable the two ways the caller can update the quantity: positive or negative changes. Ultimately the validated change is updated in place in memory for subsequent calls.

The update_price method will be similar:

The main differences in update_price from update_quantity are the validation rules about price: $0.00 priced items are not allowed, and we guard against negative prices.

Now as we add our watch implementation things will get a little bit more interesting, as we have to provide the mechanism to stream updates back out to the client. First we'll define a return type for our stream which will utilize the Stream type from Rust's futures library:

Our streams will consist of a Result<Item, Status> where each update the client receives will either contain a new copy of the Item which has changed, or a Status indicating any problems that were encountered (and corresponding with the error messages we placed in constants in a previous step).

With that we can define our watch implementation:

Note: keep in mind that all code is just for demonstration purposes, you would not necessarily want to, for instance, use unbounded channels in your production applications.

The comments throughout should hopefully provide a good walkthrough of how everything works, but these are the high level steps:

  1. validate the input
  2. create a Channel which we will stream Item data into
  3. use tokio::spawn to spawn a new asynchronous task in the background which will continue to update our client with changes to the subscribed Item until the client closes the connection, or an error occurs
  4. send the rx portion of the Channel back wrapped as our WatchStream type we defined in the previous step

With that we can add, remove, get, update and watch items in our inventory! We need a mechanism to start this server we just created, so let's add that to our src/main.rs, making the file look like this:

Note: You can notice that we are registering InventoryService and reflection_service. Reflection service is an implementation of Server Reflection specification that allows gRPC clients discover services available on gRPC clients. Next, we demonstrate how Insomnia gRPC client can learn about what methods are available in InventoryService.

In the next steps we'll move on to client code so that we can see our server in action.

Step 4.5: Testing gRPC Server with Insomnia

We're going to run our server in the background, and then try a variety of cli commands against it.

Start by creating a new separate terminal which we'll dedicate to the server and run:

At first, we are going to test our server using Insomnia.

After you downloaded and installed Insomnia, open it and click ⨁ icon and select «gRPC Request».

Enter localhost:9001 in gRPC server bar. In gRPC method dropdown, select Click to use server reflection.

Select Unary method /Inventory/Add and add body of json request.

Note: Feel free to experiment with other methods of Inventory service on your own.

Step 5: Implementing the Client

The server should be is up and running (if not, start it with cargo run --release --bin server), now we need to be able to use the generated API client to view and manage our inventory. For this we will make a command-line tool which can be used to manage the inventory using the gRPC API.

We'll use Clap, which is a popular command-line toolkit for Rust and create our CLI. Create the file src/cli.rs and add the required imports:

So we've imported the Parser from Clap so we can construct our CLI using structs with Clap attributes and we've imported the InventoryClient and some of our other relevant types from our API, now let's add our commands:

The above instructs Clap to provide the entries in the Command enum as sub-commands so that we'll be able to run demo add, demo remove, and so forth. We'll need to add the options and the implementation for each of these, so let's get started with add:

The AddOptions struct enables us to provide all the required data to add an item to the inventory, and includes helpful options like the ability to provide defaults and Options can be used for optional parameters. With this we'll be able to run things like demo add --sku 87A7669F --price 1.99 to add a new Item to the inventory.

Next up we'll handle remove, which is fairly brief:

The get functions's implementation is small as well:

Now for the update functions, which will somewhat resemble one-another, starting with update_quantity:

Our watch command is surprisingly simple, given that the implementation on the server side was fairly involved, all we need to do is receive the Stream from the watch request on the server, and iterate through it with .next():

Also you'll see that we didn't bother making a specific option struct for watch as the previously created GetOptions type is perfectly sufficient.

With all our API methods covered, now we just need to tie it all together with Clap and add our main function:

Now we're ready to test everything out!

Step 6: Trying It Out

Everything's in place, and now it's time to see our work in action!

Now let's test gRPC client using cli app we developed earlier. Then in another terminal in the work directory, let's compile the CLI and make a copy:

Now we should be ready to start making commands. Let's start by adding a new Item to the inventory:

Retrieve the item to see its contents:

Great, and let's run the exact same thing another time, to verify that our validation code rejects the duplicate:

Then we can change the quantity, as if some inventory had been purchased:

Then update the price:

Then we can watch the item as we change the inventory, in a new terminal dedicated to running watch:

Then back in our previous terminal, make several changes and even remove the item entirely:

Over in the watch terminal, you should have seen a stream of all the actions:

We've accomplished what we set out to do, we have an API with a streaming endpoint, and a CLI which exercises it. If you want to play around with it more, Clap automatically generates --help information:

At this point you should have a solid basic understanding of how to set up and test gRPC services written in Rust with the Tonic framework.

Next Steps

I hope you enjoyed this demo. If you're interested in doing more with it the code provided here was a light touch for the purposes of demonstration brevity, but there are certainly some follow-up tasks you could do if you like.

All the code is available for you on Github and if you find ways you'd like to improve it feel free to send in a pull request.

Some tasks that were not covered in this demo for time reasons were:

  • adding TLS and auth to the client and server to protect data
  • add command line flags for the cli and server, enable changing host:port
  • improve the appearance of the CLI output for better human readability

And certainly many more tasks. Or you can start your own project with what you learned here, either way happy coding!

Developer agility meets compliance and security. Discover how Kong can help you become an API-first company.