Making a CRUD Server for our Database

We've now explored a couple different libraries for some production tasks in Rust. Back in part 2, we used Diesel to create an ORM for some database types. And then last time, in part 3, we used Rocket to make a basic web server to respond to basic requests. Now, we'll put these two ideas together! We'll use some more advanced functionality from Rocket to make some CRUD endpoints for our database type. Take a look at the code on Github here!

If you've never written any Rust, you should start with the basics though! Take a look at our Rust Beginners Series!

Database State and Instances

Our first order of business is connecting to the database from our handler functions. There are some direct integrations you can check out between Rocket, Diesel, and other libraries. These can provide clever ways to add a connection argument to any handler.

But for now we're going to keep things simple. We'll re-generate the PgConnection within each endpoint. We'll maintain a "stateful" connection string to ensure they all use the same database.

Our Rocket server can "manage" different state elements. Suppose we have a function that gives us our database string. We can pass that to our server at initialization time.

fn local_conn_string() -> String {...}

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

}

Now we can access this String from any of our endpoints by giving an input the State<String> type. This allows us to create our connection:

#[get(...)]
fn fetch_all_users(database_url: State<String>) -> ... {
  let connection = pgConnection.establish(&database_url)
    .expect("Error connecting to database!");
  ...
}

Note: We can't use the PgConnection itself because stateful types need to be thread safe.

So any other of our endpoints can now access the same database. Before we start writing these, we need a couple things first though. Let's recall that for our Diesel ORM we made a User type and a UserEntity type. The first is for inserting/creating, and the second is for querying. We need to add some instances to those types so they are compatible with our endpoints. We want to have JSON instances (Serialize, Deserialize), as well as FromForm for our User type:

#[derive(Insertable, Deserialize, Serialize, FromForm)]
#[table_name="users"]
pub struct User {
  ...
}

#[derive(Queryable, Serialize)]
pub struct UserEntity {
  ...
}

Now let's see how we get these types from our endpoints!

Retrieving Users

We'll start with a simple endpoint to fetch all the different users in our database. This will take no inputs, except our stateful database URL. It will return a vector of UserEntity objects, wrapped in Json.

#[get("/users/all")]
fn fetch_all_users(database_url: State<String>)
  -> Json<Vec<UserEntity>> {
    ...
}

Now all we need to do is connect to our database and run the query function. We can make our users vector into a Json object by wrapping with Json(). The Serialize instance lets us satisfy the Responder trait for the return value.

#[get("/users/all")]
fn fetch_all_users(database_url: State<String>)
  -> Json<Vec<UserEntity>> {
    let connection = PgConnection::establish(&database_url)
      .expect("Error connecting to database!");
    Json(users.load::<UserEntity>(&connection)
      .expect("Error loading users"))
}

Now for getting individual users. Once again, we'll wrap a response in JSON. But this time we'll return an optional, single, user. We'll use a dynamic capture parameter in the URL for the User ID.

#[get("/users/<uid>")]
fn fetch_user(database_url: State<String>, uid: i32)
  -> Option<Json<UserEntity>> {
    let connection = ...;
    ...
}

We'll want to filter on the users table by the ID. This will give us a list of different results. We want to specify this vector as mutable. Why? In the end, we want to return the first user. But Rust's memory rules mean we must either copy or move this item. And we don't want to move a single item from the vector without moving the whole vector. So we'll remove the head from the vector entirely, which requires mutability.

#[get("/users/<uid>")]
fn fetch_user(database_url: State<String>, uid: i32)
  -> Option<Json<UserEntity>> {
    let connection = ...;
    use rust_web::schema::users::dsl::*;
    let mut users_by_id: Vec<UserEntity> =
          users.filter(id.eq(uid))
            .load::<UserEntity>(&connection)
            .expect("Error loading users");
    ...
}

Now we can do our case analysis. If the list is empty, we return None. Otherwise, we'll remove the user from the vector and wrap it.

#[get("/users/<uid>")]
fn fetch_user(database_url: State<String>, uid: i32) -> Option<Json<UserEntity>> {
    let connection = ...;
    use rust_web::schema::users::dsl::*;
    let mut users_by_id: Vec<UserEntity> =
          users.filter(id.eq(uid))
          .load::<UserEntity>(&connection)
          .expect("Error loading users");
    if users_by_id.len() == 0 {
        None
    } else {
        let first_user = users_by_id.remove(0);
        Some(Json(first_user))
    }
}

Create/Update/Delete

Hopefully you can see the pattern now! Our queries are all pretty simple. So our endpoints all follow a similar pattern. Connect to the database, run the query and wrap the result. We can follow this process for the remaining three endpoints in a basic CRUD setup. Let's start with "Create":

#[post("/users/create", format="application/json", data = "<user>")]
fn create_user(database_url: State<String>, user: Json<User>)
  -> Json<i32> {
    let connection = ...;
    let user_entity: UserEntity = diesel::insert_into(users::table)
        .values(&*user)
        .get_result(&connection).expect("Error saving user");
    Json(user_entity.id)
}

As we discussed in part 3, we can use data together with Json to specify the form data in our post request. We de-reference the user with * to get it out of the JSON wrapper. Then we insert the user and wrap its ID to send back.

Deleting a user is simple as well. It has the same dynamic path as fetching a user. We just make a delete call on our database instead.

#[delete("/users/<uid>")]
fn delete_user(database_url: State<String>, uid: i32) -> Json<i32> {
    let connection = ...;
    use rust_web::schema::users::dsl::*;
    diesel::delete(users.filter(id.eq(uid)))
      .execute(&connection)
      .expect("Error deleting user");
    Json(uid)
}

Updating is the last endpoint, which takes a put request. The endpoint mechanics are just like our other endpoints. We use a dynamic path component to get the user's ID, and then provide a User body with the updated field values. The only trick is that we need to expand our Diesel knowledge a bit. We'll use update and set to change individual fields on an item.

#[put("/users/<uid>/update", format="json", data="<user>")]
fn update_user(
  database_url: State<String>, uid: i32, user: Json<User>)
   -> Json<UserEntity> {

    let connection = ...;
    use rust_web::schema::users::dsl::*;
    let updated_user: UserEntity =   
          diesel::update(users.filter(id.eq(uid)))
            .set((name.eq(&user.name),
                  email.eq(&user.email),
                  age.eq(user.age)))
            .get_result::<UserEntity>(&connection)
            .expect("Error updating user");
    Json(updated_user)
}

The other gotcha is that we need to use references (&) for the string fields in the input user. But now we can add these routes to our server, and it will manipulate our database as desired!

Conclusion

There are still lots of things we could improve here. For example, we're still using .expect in many places. From the perspective of a web server, we should be catching these issues and wrapping them with "Err 500". Rocket also provides some good mechanics for fixing that.

In part 5 though, we'll pivot to another server problem that Rocket solves adeptly: authentication. We should restrict certain endpoints to particular users. Rust provides an authentication scheme that is neatly encoded in the type system!

For a more in-depth introduction to Rust, watch our Rust Video Tutorial. It will take you through a lot of key skills like understanding memory and using Cargo!