Building gRPC APIs with Rust
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.
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
protocare 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:
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
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
--experimental_allow_proto3_optionalargument isn't strictly necessary on newer systems with
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
protocis 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.
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
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
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
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 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
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.
update_price method will be similar:
The main differences in
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
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:
- validate the input
- create a
Channelwhich we will stream
tokio::spawnto spawn a new asynchronous task in the background which will continue to update our client with changes to the subscribed
Itemuntil the client closes the connection, or an error occurs
- send the
rxportion of the
Channelback wrapped as our
WatchStreamtype we defined in the previous step
With that we can
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
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
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».
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 remove, and so forth. We'll need to add the options and the implementation for each of these, so let's get started with
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:
get functions's implementation is small as well:
Now for the update functions, which will somewhat resemble one-another, starting with
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
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
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
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
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
- 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!