RP1: an experimental Diesel-based CRUD for Rocket

RP1: an experimental Diesel-based CRUD for Rocket
Most of our web applications use either Node.js or Symfony for their server-side part. Both offer a lot in terms of productivity. But every now and again, when you look at the computing power used or the amount of time a simple HTTP request takes, you can't help to think "what if..?".

So, a couple of years back, we explored Rust for the web. While we liked the promises, we felt the ecosystem wasn't quite ready. This year we've re-explored Actix and Rocket, got convinced quickly and started using both in production.

Eventually, we decided that Rocket offers more of what we need and was more suited to our style of development. Internally we were discussing how we could help Rocket progress to a stable version 0.5 (with compilation on stable rust and with async support), only to be outdone by the people working on Rocket, who recently released the first release candidate. So instead we focussed our efforts on a common use case for our company: the CRUD.

Rocket provides a very efficient and very fast way to handle HTTP requests. Meanwhile, Diesel is a complete ORM for Rust that allows efficient communication with a database. While both work really well, we wanted something to be able to rapidly prototype a REST-like API. This is why we decided to create RP1.

RP1 is a procedural macro that generates a set of useful basic CRUD (Create-Read-Update-Delete) endpoints in a REST-like API with JSON output. Let's take a look at how RP1 can help you.

Overview

When querying items from the database with Diesel you have to implement a struct that implements the diesel::Queryable trait. This struct is the base for RP1 macro. Let's look at an example:

#[rp1::crud(database = "Db", table_name = "users")]
#[derive(Debug, serde::Serialize, diesel::Queryable)]
struct User {
    #[primary_key]
    pub id: i32,
    username: String,
    role: String,
    #[generated]
    created_at: chrono::NaiveDateTime,
    #[generated]
    updated_at: chrono::NaiveDateTime,
}

The macro now generates the endpoints (and some additional utilities), which are exposed as a get_routes associated function.

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

#[rocket::launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/users", User::get_routes())
        .attach(Db::fairing())
}

This will generate these routes:

  • GET /users to get a list of all users
  • POST /users to create a new user based on a JSON or form encoded body
  • GET /users/1 to get a user with id 1
  • PATCH /users/1 to update a user with some new values (either as JSON or form encoded again)
  • DELETE /users/1 to delete a user

Additionally, the list endpoint has additional support for filtering, sorting, and pagination, all generated automatically. Make sure you take a look at the documentation, specifically take a look at the macro docs.

Challenges

The biggest challenge is that one size does not fit all. We're still figuring out at what point it makes more sense to just write the endpoints without (this level of) code generation.

An open technical problem is the retrieval of related entities. Currently, getting a user and all their posts takes two separate queries. Related entities open up a big configuration design space: what if there are multiple related entities, what if I only want 10 most recent posts for every user. We hope to experiment with possible designs in the future.

There are more challenges that need overcoming in the future. Please take a look at our project board to see some of the things we currently know are pain points or which would still need improvements. Of course, we would very much welcome any additional suggestions.

Conclusion

We learned a lot working on this project. It was our first proper introduction to writing procedural macros. We also hit some neat things in the type machinery diesel uses to encode the database as rust types and values.

Still, we're not 100% satisfied with our current solution and continue the search for an approach that is convenient to use and fast to compile. Let us know if you have ideas on how to get there.

Stay up-to-date

Stay up-to-date with our work and blog posts?

Related articles

Asynchronous programming is pretty weird. While it is straightforward enough to understand in principle (write code that looks synchronous, but may be run concurrently yada yada yada), it is not so obvious how and when async functions actually perform work. This blog aims to shed light on how that works in Rust.
The number of data centers worldwide is constantly increasing, and so is their electricity consumption. One way to become more power-efficient is certainly the constant development of better hardware, but we as developers should do our share. This post shows how coding in Rust can help to use existing resources more efficiently, to help preserve our planet — at least a little bit.
When sudo-rs development started, we added several dependencies using Rust’s crates ecosystem to quickly ramp up development. During development we accrued approximately 135 transitive (direct and indirect) dependencies. Once this was identified, we managed to reduce our total dependencies down to three. In this blog, we explain why and how we did this.