Writing an API server in Rust, using Rocket and Diesel, Part 1

7 minute read Published:

A modern tutorial on writing an API server in Rust

Join me as I bumble my way through setting up a Rust API server, using the latest versions of the Rocket web framework, the Diesel ORM, and a few less exicting things like Docker and Postgres. I decided to write this tutorial after noticing that a lot of the content for these frameworks was out of date, and there was a distinct lack of reading on how to get them all to play nicely with each other. Part 1 will go over setting everything up, and Dockerizing it for easy development and deploys.

Before we jump into implementation details, I want to put out a bit of a disclaimer: I did not choose any of the libraries used to create this tutorial based on any sort of proper research or best practice. Both Rocket and Diesel are under active development, and I can’t recommend using either in a production environment, because I just don’t know enough about them. This is strictly a “for fun” project, and you should not take my choice of libraries as a solid recommendation. I’m experimenting :) That said, I do think both Rocket and Diesel are fantastic.

A further note: I assume you have Rust installed, and are familiar with it. I may do a Rust install tutorial at a later date, but this is not that tutorial.

So, lets get started. What are we building today? Well, I want to build a REST API that I can retrieve data from. Sometimes I may want to push updates to that data. Pretty standard stuff. A few things immediately come to mind from that simple description: we need some way to handle different REST routes that mean different things, and return different responses. We need a storage mechanism for persisting the data being served. We’ll probably need some sort of authentication/throttling mechanism to control usage. Lastly, I want to point out that I will not be implementing any sort of front end for this endeavor, thats for a different time.

Based on that incredibly simple criteria, I chose the following: Rocket (we’ll be using v0.5-rc) for our web framework. Its recently been cleared to work on the stable branch of Rust, so thats a plus, and it has killer documentation. It provides a web server, route handler, and JSON serializing/deserializing, basically all we need for our simple server. It also has TYPED REQUESTS (!!!), which, coming from the Python world is a big deal, and sounds super cool.

For our database concerns, I chose to go with Postgres simply because its what I’m most familiar with. The technologies I’m outlining here can easily use any other DBMS in place of Postgres. For actually managing that database, I chose Diesel (using version 1.4.8). Diesel is an ORM that wraps around whatever DBMS you choose, and handles migration creation and application, as well as providing a query builder. Reminds me of SQLAlchemy a bit. I’ve been living in Django/Python land for a while, and having a competent ORM is a must (I’m lazy).

Finally, I want to Dockerize the entire thing, because why not.

Lets start by creating a new rust project (again, assuming you have Rust/Cargo configured):

cargo new rusty_api

Next, lets add our dependencies (I expect this list to grow, but for now, its pretty simple). In cargo.toml:

[dependencies]
rocket = { version = "0.5.0-rc.1" }
diesel = { version = "1.4.4", features = ["postgres"] }

[dependencies.rocket_sync_db_pools]
version = "0.1.0-rc.1"
default-features = false
features = ["diesel_postgres_pool"]

As of this writing, these are the latest stable versions of Rocket and Diesel, respectively. I’ll get into what exactly the rocket_sync_db_pools is in a later post, but if you want to read up, you can see that here.

Next, lets create a Dockerfile at the root of our rusty_api project:

FROM rust:1.55

RUN cargo install diesel_cli --no-default-features --features postgres

RUN cargo install cargo-watch

WORKDIR /usr/src/app

ENV ROCKET_ADDRESS=0.0.0.0

EXPOSE 8000

VOLUME ["/usr/local/cargo"]

I’m using the official Rust image here. Its probably not the smallest or the most efficient, but its perfect for what we’re trying to accomplish. Next, we’re installing diesel_cli which, as the name implies, is a CLI tool for the Diesel ORM. We’re specifiying that we want to use it with a Postgres database. I’m also installing cargo-watch for hot reloading if I change anything on the container (this one is optional, and I may remove it, depending on if it proves useful or not, but it doesn’t hurt anything to have it).

Next, I’m setting the working dir, and binding Rocket to localhost so we can easily access it, and exposing port 8000 (the port Rocket will be running on within the container). Finally, I’m setting a volume for cargo to cache stuff on, so it doesn’t try and re-download it all every time we bring the container up.

Next, lets add some server code to src/main.rs:

#[macro_use] extern crate rocket;

use rocket_sync_db_pools::{diesel, database};

#[get("/")]
fn index() -> &'static str {
    "Hello, World!"
}

#[catch(503)]
fn service_not_available() -> &'static str {
    "Service not available..."
}

#[database("rusty_api_db")]
struct RustyAPIDbConn(diesel::PgConnection);

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(RustyAPIDbConn::fairing())
        .register("/", catchers![service_not_available])
        .mount("/", routes![index])
}

This is mostly ripped straight out of the Rocket tutorial with a few additions. Our base route, defined with the #[get('"/")] macro, will return a response containing the text “Hello, World!”. I’ve also defined a method to catch 503 errors, and return an appropriate message (in the case our Postgres DB or container is unavailable for some reason). Next, we’re defining a connection to our DB, more on this in the future, but Diesel manages that pretty handily for us.

Finally, the #[launch] macro kicks off a Rocket server. We attach our DB to the service, using a Fairing (the rocketry theme is strong here), and then we register our catcher on our primary route, and then mount our index function to our primary route. Pretty straight forward, and if you’re familiar with any other lightweight web frameworks, there should really be no surprises here.

A couple more files to create, and we can move on. I created a .env file to store a definition of our database:

DATABASE_URL=postgres://rustyapi_user:admin@database/rustyapi

Nothing complicated, just a Postgres url.

Finally, we’re going to create a rocket.toml file to store some additional Rocket config in:

[global]
address = "0.0.0.0"
port = 8000

[global.databases]
rustyapi_db = { url = "postgres://rustyapi_user:admin@database/rustyapi" }

Very similar to above, might not be needed, but I’m learning, and it’s presence doesn’t break anything, so, it stays for now until I know better.

Alright, that does it for our Rust app (for now). The way I’ve set up my application thus far, is an outer directory , containing my docker-compose.yml, and then my Rust app. I set things up this way in the event that I ever wanted to add a UI on top of the Rust API, it seemed easier to have a central docker-compose.yml from the start, rather than move it when I want to orchestrate more containers later. So, one directory higher than our Rust app, I have a docker-compose.yml that looks like:

# docker-compose.yml
version: "3"

services:
  api_server:
    build: ./rustyapi_server
    ports:
      - "8000:8000"
    volumes:
      - ./rustyapi_server:/usr/src/app
    links:
      - db
    command: bash -c "diesel setup && cargo watch -x run"
    depends_on:
      database:
        condition: service_healthy

  database:
    image: "postgres"
    ports:
      - "5432:5432"
    env_file:
      - database.env
    volumes:
      - database-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U rustyapi_user -d rustyapi"]
      interval: 10s
      timeout: 5s
      retries: 5

# cargo attempts to re-download packages, so cache them here
volumes:
  database-data: {}

Only a couple of things of note in here: First, the diesel setup command, this will initialize deisel with out app. It creates a diesel.toml file for settings, and also creates our first schema, by creating a migrations directory, and creating a first set of up and down migrations. I’ll go into more detail on this is a future post what these files do, but for suffice to know that they setup some metadata tables, with some triggers on them, and then applies all of them.

The other thing of note is the healthcheck. we need to be sure our DB container is fully up, with Postgres running prior to starting our Rust container with Diesel, or else we’ll get errors. Nothing fancy.

And thats it. At this point, we can docker-compose up in our top directory, wait a bit, and once our Rust app has built, and we get something like this from Rocket:

Rocket has launched from http://0.0.0.0:8000

We can navigate to localhost:8000 in a browser, and hopefully, see our little “Hello, World!” response.

Next time, we’ll be diving into Diesel, and creating out first set of migrations to store some data, which we’ll later be serving with Rocket.