Build a CRUD API with Rust

Since my initial Node/Rust REST comparison, I’ve wanted to follow up with a comprehensive guide for getting simple CRUD operations up and running in Rust.

While Rust has its complexities, coming from Node.js, one thing that’s kept me attracted to the language is its simplicity. It’s sleek and strikes a great balance between safety and cognitive intuition.

In any event, hopefully this guide helps those that are new to Rust and encourages those who are on the fence.

Scroll down if you’d like to see how Rust compares against Java and Node.js

Major frameworks used

  • Rocket — web framework for writing fast web applications
  • Serde — framework for serializing and deserializing Rust data structures
  • Diesel — safe, extensible ORM and query builder

Creating our application in Rust

First, we’ll create a new Rust project using the commands below.

[sean@lappy486:~] $ cargo new hero-api --bin && cd hero-api
Created binary (application) `hero-api` project

Before we move forward though, Rocket requires us to use the nightly Rust build. Thankfully, there’s a quick command we can use to switch channels.

$ rustup default nightly
$ rustup update && cargo update

You’ll get some output here regarding the switch but, can confirm it was successful by invoking the --version flag on cargo and rustc

$ cargo --version && rustc --version
cargo 1.26.0-nightly (5f83bb404 2018-03-09)
rustc 1.26.0-nightly (55c984ee5 2018-03-16)

Adding our first Rust dependency (Rocket)

Alright, within our hero-api directory, you’ll find a Cargo.toml and src folder. Cargo.toml will act similar to Node’s package.json which we’ll cover later — and I suspect src is descriptive enough :)

We’ll need to add Rocket as a dependency by editing Cargo.toml and adding the following two dependencies. There’s certainly more to add later but, this will at least get us started.

[dependencies]
rocket = "0.3.6"
rocket_codegen = "0.3.6"

Now we can edit src/main.rs — we’ll just borrow the starter code provided to us by Rocket.rs to ensure everything works at this point.

The thing that I find most appealing about Rust (and Rocket) is how self descriptive the majority of the code is.

  • #[get("/<name>/<age>")]

Here we expose a single endpoint, leveraging Rocket’s annotation, that takes two parameters <name> and <age>. We then map those parameters to our route handler and specify their types — if the types are unmatched, the route handler will not be invoked and a 404 will be returned.

  • fn hello(name: String, age: u8) -> String {

Our route handler returns a formattedString, using Rust’s format! macro, with the supplied name and age. Notice no trailing semicolon for return statements

  • format!("Hello, {} year old named {}!", age, name)

The main method, similar to other languages, is aptly named main. This is the first function that’s invoked when our application runs. Here we tell Rocket to startup and mount our route hander using /hello as the root context. This provides us with a URL like: /hello/<name>/<age>.

fn main() {
rocket::ignite()
.mount("/hello", routes![hello])
.launch();
}

Your two files should look similar to the image below. Let’s test that everything works by starting our server using thecargo run command. This should start a server on localhost:8000 which can be hit via browser/curl.


Building Restful endpoints using JSON (Serde)

So, one endpoint will only get us so far. Thinking about our API, we want simple CRUD operations to manage Hero data; so right off the top, we’ll want:

  • Create:POST /hero
  • Read: GET /heroes
  • Update: PUT /hero/:id
  • Delete: DELETE /hero/:id

Now that we have that out of the way — we’ll be using JSON as our primary means for exchanging data, so in comes our next dependency: Serde.

This will require the following additions to our Cargo.toml:

We’ll also want think about what properties a given Hero should have. Below we create a simple Hero model within src/hero.rs — adding the Serialize and Deserialize annotations from Serde so that our model can be extracted from and converted to JSON.

id is Optional since consumers hitting our CREATE endpoint will not yet have an id to send. However, when we retrieve data and supply it as a response, anything from our DB will have an id established.

Let’s go ahead and create our CRUD endpoints now, using dummy data for the time being:

In the above, we include our new dependencies and reference our Hero type, we also pull out Json and Value from rocket_contrib; this will make it easier dealing with JSON requests/responses.

#[macro_use] extern crate rocket_contrib;
#[macro_use] extern crate serde_derive;
use rocket_contrib::{Json, Value};
mod hero;
use hero::{Hero};

We also add our other operations (POST, PUT, DELETE). The data attribute within our route annotation simply tells Rocket to expect Body Data — then map the body to a parameter. Here we say, the body expected should be in the form of a Hero but wrapped in JSON.

#[post("/", data = "<hero>")]
fn create(hero: Json<Hero>) -> Json<Hero> {

We should now be able to hit any of our configured endpoints using a standard rest client/curl, etc.


Persisting our data via ORM (Diesel)

I think we can all agree, having a few URLs is great but, not too helpful if the data we’re sending isn’t persisted. For this, we’ll use Diesel as it’s currently one of the most mature Rust ORM frameworks.

Doing this for the first time, I’ll admit, is certainly an involved process but once the initial bootstrap is out of the way, it works quite well. I’ve tried this with the blackbeam raw mysql driver, and while it works, the codebase becomes polluted really quick..

In fact, Sean Griffin (author of Diesel) wrote a great article illustrating this very point.

To get started, we install the Diesel CLI:

$ cargo install diesel_cli

Then we tell Diesel where our database should live and run setup:

$ export DATABASE_URL=mysql://user:pass@localhost/heroes
$ diesel setup
Creating database: heroes

Next, we’ll generate a migration strategy — this basically allows us to keep revisions as our database evolves over time.

$ diesel migration generate heroes
Creating migrations/2018-03-17-180012_heroes/up.sql
Creating migrations/2018-03-17-180012_heroes/down.sql

We’ll need to edit these two files to include the SQL for our Heroes schema

Now we can run our migration — which will execute up.sql against our DB

[sean@lappy486:~/hero-api] $ diesel migration run
Running migration 2018-03-17-180012_heroes

With any luck, that’s the last SQL we should touch for this project. Let’s go ahead and add Diesel to our dependencies in Cargo.toml. We’re also going to add r2d2-diesel which will allow us to manage db connection pooling.

[dependencies]
diesel = { version = "1.0.0", features = ["mysql"] }
diesel_codegen = { version = "*", features = ["mysql"] }
r2d2 = "*"
r2d2-diesel = "*"

We’ll create src/db.rs to establish our database connection and manage our pool — thankfully, most of this was provided by Rocket Connection Guard starter code

We’ll also need to make the following additions to bind our Hero model with the new table info we’ve created:

This enables us to create an auto-generated schema derived from our struct

$ diesel print-schema > src/schema.rs

However, we’re going to make a small change so that we can leverage the same model for Queryable and Insertable object. The Diesel author expressed fair reasoning for separating our object into two models but, I prefer the convenience and less code pollution. So for that, we edit src/schema.rs 😅

Without the change above, we’d require two models: one to insert (without id) and a separate to retrieve (with id).

Now we can import these new dependencies by adding the following intosrc/main.rs:

#[macro_use] extern crate diesel;
extern crate r2d2;
extern crate r2d2_diesel;
mod db;
mod schema;

Next we’ll tell Rocket to manage our db connection pool by adding the following to our rocket::ignite() chain:

.manage(db::connect())

Believe it not, now we’re ready to get this thing going! Here we’ll expose a few methods by creating an implementation for Hero within src/hero.rs — save for more sophisticated error handling.

Now we can leverage the newly created methods within our route handlers

Notice, each method within our route handlers and Hero implementation now essentially accept the same/similar arguments: Some combination of an id, Hero object and database connection.

For more info on how to query data, refer to the Diesel Getting Started guides.

Finally, we can now create a Hero and see real results in our REST Client. Fetching, updating and deleting Heroes also work as expected.


Putting our API to the test (wrk)

Now that we have a somewhat complete API, it’s probably worth benchmarking this thing as one of the most touted aspect of Rust is its performance over other languages.

But before trying it out, I wanted to get an idea of how other popular languages and frameworks stack up.

For this, I simply used wrk locally on my Macbook Pro with no other user applications or services running.

MacBook Pro (15-inch, 2016) 
Processor: 2.9 GHz Intel Intel Core i7
Memory: 16 GB 2133 MHz LPDDR3
Storage: 1 TB Flash Storage

Java 1.8 (Spring Boot, Hibernate)

I threw together a quick example exposing the same GET Heroes method that we created in our Rust API above by following a great tutorial by Rajeev Kumar Singh (found here)

The example code used can be found here:

The results weren’t bad @ 4,584 requests per second

Node 9.8 (Restify, Sequelize)

The Node team has made significant improvements in Node 9. Now leveraging V8’s Ignition and Turbofan — Node’s performance has nearly tripled.

The example code used can be found here:

The results weren’t nearly what I was hoping @ 2,506 requests per second. It’s worth noting that using the raw mysql package nearly doubled the handled requests per second.

Rust 1.26 (Rocket, Diesel)

Finally, to test our Rust server, we’ll want to ensure ROCKET_ENV is set to prod, otherwise it will run in development mode.

$ cargo build --release && cd target/release/
$ sudo ROCKET_ENV=prod ./hero-api
🚀 Rocket has launched from http://0.0.0.0:80

The example code used can be found here:

Rust seriously outperformed both of the other frameworks


Conclusion

Node provided the smallest code footprint but, obviously without type safety and on the lower end of the performance scale — although, 2k req/sec is nothing to scoff at. We can improve Node’s performance at the cost of nastier code maintenance.

Java had by far the largest code footprint. It also takes the longest to start up at an average ~4seconds. Not really much else to report here, it just wasn’t.. fun to develop?

Rust is a tremendously fun language to use and it’s blazingly fast. It goes without saying, speed isn’t the only factor when considering the right tool for the job but, it’s certainly an important one.

To learn more about Rust, check out the “The Rust Programming Language