Create a REST API Layer

Since our GUI is going to run in the browser, we need something for the browser to talk to. There are other choices we could make, but REST is the most sensible choice we can make for this app in 2019. So that's what we're going to do!

Rocket is a web framework for rust that will make it easy for us to quickly write a rust program to serve our API.

Add rocket to Cargo.toml

We need to add rocket into our app. Make your Cargo.toml's dependencies section look like:

[dependencies]
diesel = { version = "1.0.0", features = ["sqlite"] }
rocket = "0.4.2"

Run cargo build to compile all the new dependencies.

Create the Backend Binary

We'll write our backend in src/bin/backend.rs. Let's make a first rough pass:

#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use]
extern crate rocket;

use mytodo::db::{query_task, establish_connection};

#[get("/tasks")]
fn tasks_get() -> String {
    "this is a response\n".into()
}

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

The first line enables a couple of features that rocket's macros need. These are experimental features, and this is part of why we need to build with rust nightly instead of stable.

Then we pull in rocket's macros.

Then we've got the handler for our route. For the moment it just returns a static string.

In our main we create a rocket instance, mount our handler, and start it.

Now if you cargo run --bin backend it will compile everything and start our backend listening on localhost port 8000. If you open your browser to http://localhost:8000/tasks (or use curl) you will see "this is a response".

Query the Database

So far we've got a pretty lame API. It just spits out a static string. It's cool that we can create a functioning web server from so little code, but we want to return dynamic data -- info pulled from the database.

We've basically already written this code -- it's nearly the same as the subcommand in our CLI program:

#[get("/tasks")]
fn tasks_get() -> String {
    let mut response: Vec<String> = vec![];

    let conn = establish_connection();
    for task in query_task(&conn) {
        response.push(task.title);
    }

    response.join("\n")
}

Run this (cargo will rebuild the changes for you), and refresh your browser -- you should see the task titles we inserted earlier. Great! But also, not so great -- we might want to add more data to the tasks in the future as our app gets popular. Like done-ness, due dates, and priority.

Just printing a bunch of lines of output is going to be tedious and error-prone for our frontend to parse.

We can make it easier for our frontend and our backend to communicate with each other if we use a standard data serialization format for our API, and likewise if we use some standard tools to do it.

That format will be JSON. There are lots of tools for dealing with JSON, and luckily for us one of them is part of rocket. The other tool we need is the excellent serde framework for serializing and deserializing data (in JSON and many other formats).

Serializing to JSON

We need to add serde and JSON support from rocket_contrib to our Cargo.toml:

[package]
name = "mytodo"
version = "0.1.0"
authors = ["Your Name <[email protected]>"]
edition = "2018"

[dependencies]
diesel = { version = "1.0.0", features = ["sqlite"] }
rocket = "0.4.2"
serde = { version = "1.0", features = ["derive"] }

[dependencies.rocket_contrib]
version = "0.4.2"
default-features = false
features = ["json"]

And then we need to use rocket_contrib and serde in backend.rs:

#[macro_use]
extern crate rocket_contrib;
#[macro_use]
extern crate serde;

Let's think about how we want to format our response. We could just send back an array of tasks, where each task is an object with a title key that has a string value:

[
    { "title": "do the thing" },
    { "title": "get stuff done" }
]

One problem with this is that we have no uniform way to indicate an error. We'll be better off if we're closer to complying with the JSON API spec, which requires an object at the top level, and a data key at that level -- something like this:

{
    "data": [
        { "id": 1, "title": "do the thing" },
        { "id": 2, "title": "get stuff done" },
    ]
}

Note that this isn't strictly conforming with the JSON API spec because the resource objects (the stuff in the data array) aren't formatted properly. But for the sake of keeping this exposition simple we're going to settle for being non-compliant for now -- see the exercises for an approach to getting this done the right way.

Now that we know what we want to return, let's put together a rust structure to represent it:

#[derive(Serialize)]
struct JsonApiResponse {
    data: Vec<Task>,
}

Here we're using the Serialize derive-macro from serde for our struct. This makes it so that we can magically get JSON out of it. However, if we try to build this we get an error:

error[E0277]: the trait bound `mytodo::db::models::Task: serde::ser::Serialize`
    is not satisfied

We can only Serialize a struct if all the things in the struct also implement Serialize -- and Task doesn't do that... yet.

Can we fix it? YES WE CAN!

At the top of both lib.rs and backend.rs we need to enable serde macros:

#[macro_use]
extern crate serde;

And then in db/models.rs we need to slap a Serialize on the Task struct:

#[derive(Queryable, Serialize)]
pub struct Task {
    pub id: i32,
    pub title: String,
}

Our handler function will use the Json type from rocket_contrib -- note that this is a different type than serde's Json type! We need to add a use declaration for it at the top of backend.rs:

use rocket_contrib::json::Json;

With all of that in place we can modify our handler function to push the tasks we get back from query_task onto a response object, and then convert that to Json on the way out:

#[get("/tasks")]
fn tasks_get() -> Json<JsonApiResponse> {
    let mut response = JsonApiResponse { data: vec![], };

    let conn = establish_connection();
    for task in query_task(&conn) {
        response.data.push(task);
    }

    Json(response)
}

Run the backend, refresh your browser, and you should see json similar to the sample above. (It won't be pretty-printed -- you can curl --silent http://localhost:8000/tasks/ | jq . if you're into that.)

REST API Layer Wrap-Up

We just built a functional REST API backend, and I don't know about you, but I didn't even break a sweat. Of course, there are things we'd do differently in a production app:

  • testing, of both the unit and integration varieties
  • documentation, from comments to docstrings to REST API user (developer) docs
  • stricter conformance to JSON API
  • API versioning
  • database connection pooling so that we don't have to do establish_connection for every request, which would be important under load
  • make our response object use a parameterized type so that we can return different object types as the API grows new features

In the next chapter we will place the final layer -- a browser-based UI based on the Seed framework. But first -- some exercises!

REST API Layer Exercises

These exercises are more challenging and will require consulting more external documentation than in the last chapter.

Bring the API in conformance with the JSON API Spec

Specifically, this part:

A resource object MUST contain at least the following top-level members:

  • id
  • type

Exception: The id member is not required when the resource object originates at the client and represents a new resource to be created on the server.

In addition, a resource object MAY contain any of these top-level members:

  • attributes: an attributes object representing some of the resource‚Äôs data.

To do this, create a new Task wrapper struct that contains the id, type, and attributes, modify the JsonApiResponse struct to contain a vector of that wrapper, and modify the loop around task_query to create and push this type onto the response.

Use Connection Pooling

Read the rocket documentation on database connection pooling and implement it in backend.rs.