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.
Our zlib-rs project implements a drop-in replacement for libz.so, a dynamic library that is widely used to perform gzip (de)compression.
Let's be frank: Rust is a cool language, but there's not a chance I'm introducing it in my company if I can't get any engineers for it. We'll stick with technologies with a much healthier job market.