RP1: an experimental Diesel-based CRUD for Rocket
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 usersPOST /users
to create a new user based on a JSON or form encoded bodyGET /users/1
to get a user with id 1PATCH /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.