Alexey's blog

A webapp skeleton with Rust, Rocket, and Diesel

11 minute read Published: 2018-09-17

This is a condensed walkthrough of building a skeleton Rust web app using Rocket, Diesel and PostgreSQL heavily based on a more thorough series of blog posts called "Making a simple blog with Rust" and updated to incorporate the recent API changes as well as using a different database schema and a few extra tools.

For a more detailed explanation of each section, please refer to the original blog:

Making a simple blog with Rust - Part I

Making a simple blog with Rust - Part II

I am using Rust version 1.30.0-nightly (6e0f1cc15 2018-09-05).

The code for this project is available on my GitHub.

First things first

I wanted to make a very basic skeleton for a prelaunchr type web app with a stripped-down schema called voskhod.

First, I started a new Rust project:

cargo new voskhod

This generated a new binary (application) project as opposed to a library one which you can get by adding a --lib flag to the above command. The difference is in the template used for the directory structure.

This is my initial directory structure:

.
├── Cargo.toml
└── src
    └── main.rs

I've been using cargo-edit to streamline adding dependencies. It provides a cargo add subcommand which, when no version is specified, will try to query the latest version's number from crates.io and then add that package to Cargo.toml.

Here's the command I used to add the dependencies needed:

cargo add rocket rocket_codegen rocket_contrib serde serde_derive serde_json tera diesel r2d2 r2d2-diesel dotenv

Note that I had to edit some of the dependencies to enable certain features.

This is what my Cargo.toml looked like after adding those edits, note that I did not need to add diesel_codegen:

# Cargo.toml

[package]
name = "voskhod"
version = "0.1.0"
authors = ["Alexey Zabelin <[email protected]>"]

# This creates an executable that can be targeted with `cargo run` and `cargo build`
# For this project, there will be two `[[bin]]` sections later on.
# More information is available at https://doc.rust-lang.org/cargo/reference/manifest.html#configuring-a-target
[[bin]]
name = "voskhod"
path = "src/bin/main.rs"

# Name of the library that will be generated.
[lib]
name = "voskhod_lib"
path = "src/lib.rs"

# Dependencies added with `cargo add` and edited to enable additional features.
[dependencies]
rocket = "0.3.16"
rocket_codegen = "0.3.16"
rocket_contrib = { version = "0.3.16", default-features = false, features = ["tera_templates"] }
serde = "1.0.76"
serde_derive = "1.0.76"
serde_json = "1.0.26"
tera = "0.11.14"
diesel = { version = "1.3.2", features = ["postgres"] }
r2d2 = "0.8.2"
r2d2-diesel = "1.0.0"
dotenv = "0.13.0"

I needed to adjust the directory structure to prevent cargo build from failing, here's what it looked like:

├── Cargo.toml
└── src
    ├── bin
       └── main.rs
    └── lib.rs

Installing diesel_cli

I ran following command:

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

Then I created a .env file with a DATABASE_URL environment variable in it in the root of the project for diesel_cli to be able to connect to it.

This is what it looked like:

DATABASE_URL=postgres://username:password@localhost/voskhod

Then I ran diesel setup to create a database for this project. It is possible that you'd encounter errors at this stage, most likely they'll be related to configuring your postgres users and roles.

Here's the output of that command:

Creating migrations directory at: /home/alexeyzab/code/voskhod/migrations
Creating database: voskhod

The blog post I am basing this on is describing how to create a blog with users and posts. But in my case, I was going for a slightly simpler schema based on this. I created the initial migration by running the following:

diesel migration generate create_users

And the output of that command was:

Creating migrations/2018-09-08-230625_create_users/up.sql
Creating migrations/2018-09-08-230625_create_users/down.sql

I edited both of those migration files:

/* up.sql */

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  email VARCHAR UNIQUE NOT NULL,
  referral_code VARCHAR UNIQUE NOT NULL,
  referrer_id INTEGER,
  created_at TIMESTAMP NOT NULL,
  updated_at TIMESTAMP
);

/* down.sql */

DROP table users;

This let me use diesel migration run to run the migration. You can also rerun a migration by using diesel migration redo.

diesel migration list showed me that the migration was complete:

Migrations:
  [X] 00000000000000_diesel_initial_setup
  [X] 2018-09-08-230625_create_users

Setting up Rocket

I started with a basic "Hello, World!" Rocket setup:

That involved changing src/bin/main.rs to match the following:

#![feature(plugin, custom_derive)]
#![plugin(rocket_codegen)]

extern crate rocket;

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

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

I created an index route and mounted the route at the / path. Rocket guide has a lot more information on the inner workings of the framework and I'll be referring to it later on.

I used cargo run and opened localhost:8000 in my browser to see the "Hello, World!" text.

Setting up Tera templates

Next, I needed to configure a base Tera layout.

I created a template directory and a base layout file like so:

mkdir templates
touch templates/base.html.tera

Then I edited the template file to look like this:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <div class="container">
          <p>Check out this tera Context:</p>
          <p>{{ my_message }}</p>
        </div>
    </body>
</html>

Then it was time to hook up the template to the Rocket app by using fairings and tera::Context.

I edited src/bin/main.rs to look like this:

#![feature(plugin, custom_derive)]
#![plugin(rocket_codegen)]

extern crate rocket;
extern crate rocket_contrib;
extern crate tera;

use rocket_contrib::Template;
use tera::Context;

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

#[get("/")]
fn index() -> Template {
    let mut context = Context::new();

    context.add("my_message", "Hello, Template World!");
    // Where `base` is the name of the template
    Template::render("base", &context)
}

Navigating to localhost:8000 now should show the new message and use the base template.

Preparing the models

infer_schema! doesn't seem to be needed anymore so my src/schema.rs looks like this:

table! {
    users (id) {
        id -> Int4,
        email -> Varchar,
        referral_code -> Varchar,
        referrer_id -> Nullable<Int4>,
        created_at -> Timestamp,
        updated_at -> Nullable<Timestamp>,
    }
}

which was generated by diesel when I ran the migrations. You can also generate a table! macro for each of your tables manually by running diesel print-schema, which connects to your DB and gets all the information needed.

Next, I needed to add the actual models. When it comes to diesel, you need to have multiple models for the same table, based on whether you're querying that information or inserting it in the database.

First I ran cargo add chrono because I am using chrono::NaiveDateTime for the timestamps, I also needed to add chrono to the list of features for diesel in Cargo.toml as mentioned here and after that I added the following code to src/models.rs:

// Bringing the schema into scope
use schema::users;
use chrono::NaiveDateTime;

#[derive(Debug, Queryable)]
pub struct User {
    pub id: i32,
    pub email: String,
    pub referral_code: String,
    pub referrer_id: Option<i32>,
    pub created_at: NaiveDateTime,
    pub updated_at: Option<NaiveDateTime>,
}

#[derive(Debug, Insertable)]
#[table_name="users"]
pub struct NewUser {
    pub email: String,
    pub referral_code: String,
    pub created_at: NaiveDateTime,
    pub updated_at: Option<NaiveDateTime>,
}

As you can see there are two structs: User and NewUser. The first struct derives the Queryable trait which indicates that this record can be queried from the database. NewUser derives the Insertable trait which, as the name suggests, represents that a structure can be used to insert a new row into the database which is specified in the #[table_name="users"] clause.

It's worth pointing out that you can generate the schema with diesel_cli_ext. However, it will only generate the Queryable model. To use diesel_cli_ext you have to invoke it from the folder that contains schema.rs.

I needed to update the src/lib.rs module to include our schema and models:

pub mod schema;
pub mod models;

Trying to cargo build then resulted in the following error:

error[E0658]: The attribute `table_name` is currently unknown to the compiler and may have meaning added to it in the future (see issue #29642)
  --> src/models.rs:16:3
   |
16 | #[table_name="users"]
   |   ^^^^^^^^^^
   |
   = help: add #![feature(custom_attribute)] to the crate attributes to enable

error: aborting due to previous error

This can be fixed by enabling macros for diesel in src/lib.rs like so:

#[macro_use]
extern crate diesel;

This made the build succeed, however, there were lots of warnings like these:

warning: cannot find type `users` in this scope
  --> src/models.rs:15:17
   |
15 | #[derive(Debug, Insertable)]
   |                 ^^^^^^^^^^ names from parent modules are not accessible without an explicit import
   |
   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
   = note: for more information, see issue #50504 <https://github.com/rust-lang/rust/issues/50504>

This GitHub issue explains why these warnings appear and mentions that it's possible to mute them on a per-module basis by using the following:

#![allow(proc_macro_derive_resolution_fallback)]

I've added it to both src/models.rs and src/schema.rs which made the warnings disappear.

Database connection

This was fairly straightforward, the way to setup your DB connection is described in detail in Rocket's guide.

I changed my src/lib.rs to look like this:

#![allow(proc_macro_derive_resolution_fallback)]
#[macro_use]
extern crate diesel;

extern crate dotenv;

extern crate r2d2;
extern crate r2d2_diesel;

extern crate rocket;
extern crate rocket_contrib;

extern crate chrono;

pub mod schema;
pub mod models;

use dotenv::dotenv;
use diesel::prelude::*;
use r2d2::{Pool, PooledConnection};
use r2d2_diesel::ConnectionManager;
use rocket::{Outcome, Request, State};
use rocket::http::Status;
use rocket::request::{self, FromRequest};
use std::env;
use std::ops::Deref;

// Type alias for the connection pool
type PgPool = Pool<ConnectionManager<PgConnection>>;

pub fn create_db_pool() -> Pool<ConnectionManager<PgConnection>> {
    dotenv().ok();

    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    let manager = ConnectionManager::<PgConnection>::new(database_url);
    Pool::new(manager).expect("Failed to create pool.")
}

// Tuple-struct wrapper that we can write an implementation of `FromRequest` for
pub struct DbConn(PooledConnection<ConnectionManager<PgConnection>>);

impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
        let pool = request.guard::<State<PgPool>>()?;

        match pool.get() {
            Ok(conn) => Outcome::Success(DbConn(conn)),
            Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
        }
    }
}

impl Deref for DbConn {
    type Target = PgConnection;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

It is almost the same as in the "Making a simple blog with Rust", with the exception of adjusting for a change in r2d2 API, namely the removal of Config.

Seeding the DB

First I created src/bin/seed.rs, then I edited Cargo.toml to include it as an extra binary:

[[bin]]
name = "seed"
path = "src/bin/seed.rs"

Trying to cargo build now will fail since there is no main function in src/bin/seed.rs.

At this point I also added the fake crate for generating the seed data:

cargo add fake

Since my schema is a bit different than the original blog post's one, I also added the following code based on this snippet to src/lib.rs to generate a referral code:

extern crate rand;

pub fn generate_referral_code() -> String {
    thread_rng().sample_iter(&Alphanumeric).take(30).collect()
}

Here's what I added to src/bin/seed.rs to seed the DB with my schema:

extern crate voskhod_lib;
extern crate chrono;
extern crate diesel;

#[macro_use] extern crate fake;

use chrono::prelude::{Utc};
use diesel::prelude::*;
use voskhod_lib::*;
use voskhod_lib::models::*;

fn main() {
    use schema::users::dsl::*;

    let connection = create_db_pool().get().unwrap();

    diesel::delete(users).execute(&*connection).expect("Error deleting users");

    fn generate_user_info() -> NewUser {
        NewUser {
            email: fake!(Internet.free_email),
            referral_code: generate_referral_code(),
            created_at: Utc::now().naive_utc(),
            updated_at: None,
        }
    }

    let new_user_list: Vec<NewUser> = (0..10)
        .map ( |_| generate_user_info() )
        .collect();

    diesel::insert_into(users)
        .values(&new_user_list)
        .get_results::<User>(&*connection)
        .expect("Error inserting users");
}

After that, running cargo run --bin seed seeded the database, which I was able to confirm by running psql voskhod and then SELECT * FROM users;.

I needed to add the information about the users to the template context. Here's how I changed src/bin/main.rs:

#![feature(plugin, custom_derive)]
#![plugin(rocket_codegen)]

extern crate voskhod_lib;
extern crate diesel;
extern crate rocket;
extern crate rocket_contrib;
extern crate tera;

use diesel::prelude::*;
use voskhod_lib::*;
use voskhod_lib::models::*;
use rocket_contrib::Template;
use tera::Context;

fn main() {
    rocket::ignite()
        .manage(create_db_pool())
        .mount("/", routes![index])
        .attach(Template::fairing())
        .launch();
}

#[get("/")]
fn index(connection: DbConn) -> Template {
    use schema::users::dsl::*;

    let mut context = Context::new();

    let user_list = users.load::<User>(&*connection).expect("Error loading users");

    context.add("users", &user_list);
    // Where `base` is the name of the template
    Template::render("base", &context)
}

And, similar to the original posts, I also needed to make sure the models can be serialized by first changing the chrono entry in Cargo.toml:

chrono = { version = "0.4.6", features = ["serde"] }

then adding this to src/lib.rs:

#[macro_use]
extern crate serde_derive;

and changing src/models.rs to:

#![allow(proc_macro_derive_resolution_fallback)]
// Bringing the schema into scope
use schema::users;
use chrono::NaiveDateTime;

#[derive(Debug, Queryable, Serialize)] // Now also deriving `Serialize`
pub struct User {
    pub id: i32,
    pub email: String,
    pub referral_code: String,
    pub referrer_id: Option<i32>,
    pub created_at: NaiveDateTime,
    pub updated_at: Option<NaiveDateTime>,
}

#[derive(Debug, Insertable, Serialize)] // Now also deriving `Serialize`
#[table_name = "users"]
pub struct NewUser {
    pub email: String,
    pub referral_code: String,
    pub created_at: NaiveDateTime,
    pub updated_at: Option<NaiveDateTime>,
}

And finally I edited the base template to display each user's email and referral code:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <div class="container">
          <h1>Users and referral codes!</h1>
          {% for user in users %}
            <p>{{ user.email }} {{ user.referral_code }}</p>
          {% endfor %}
        </div>
    </body>
</html>

Running cargo run --bin voskhod and navigating to localhost:8000 showed the following:

Users_Referral_codes

Conclusion

After all that is done, you are left with a basic web app skeleton that you can expand on. I am somewhat curious about what this setup would look like with Actix so I am planning on exploring that next.