Minimalistic todo app with storage based on PostgreSQL DB with Diesel ORM - probably the first mature rust orm, currently used in crates.io and many other projects.
It has a number of advantages - stability, feature-completeness, plenty of configs and utility crates, and easy to use once you've set it up. High-performance and low-risk choice for lots of projects. However, the initial setup might be tricky because diesel crates link to host-provided db client libraries.
This example is using diesel-async because the rest of the server is async, and it's intended to showcase basic apis with a UI similar to other DB examples to easily compare their usage. To get started with this one you'll need:
cargo install diesel_cli --no-default-features --features postgres
- install diesel CLI that you'll need for common diesel-related opsdocker run -p 5432:5432 -e POSTGRES_PASSWORD=password -d postgres
- start a postgres instance in a docker containercd examples/databases/postgres-diesel && diesel setup --migration-dir="./migrations/"
- setup database & migrationscargo run -p postgres-diesel
- to start the example
It's powered by a few additional dependencies in the manifest:
Cargo.toml
[package]
name = "postgres-diesel"
edition = "2021"
[[bin]]
name = "serve"
path = "./serve.rs"
[dependencies]
prest = { path = "../../../", version = "0.4" }
diesel = { version = "2.1.0", features = ["uuid"] }
diesel-async = { version = "0.3.1", features = ["deadpool", "postgres"] }
A separate manifest for the diesel configuration:
diesel.toml
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "./schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId"]
[migrations_directory]
dir = "./migrations"
A model that defines the auto-generated schema:
models.rs
use diesel::{pg::Pg, prelude::*};
use prest::{Deserialize, Serialize, Uuid};
#[derive(Queryable, Selectable, Insertable, Serialize, Deserialize)]
#[diesel(table_name = crate::schema::todos)]
#[diesel(check_for_backend(Pg))]
pub struct Todo {
#[serde(default = "Uuid::now_v7")]
pub uuid: Uuid,
#[serde(default)]
pub task: String,
#[serde(default)]
pub done: bool,
}
And a prest app that uses all of the above to manage todos:
serve.rs
pub mod models;
pub mod schema;
use prest::*;
use diesel::prelude::*;
use diesel_async::{
pooled_connection::{deadpool::Pool, AsyncDieselConnectionManager},
AsyncPgConnection, RunQueryDsl,
};
use models::Todo;
use schema::todos::dsl::*;
state!(DB_POOL: Pool<AsyncPgConnection> = {
let database_url = "postgres://postgres:password@localhost/prest";
let config = AsyncDieselConnectionManager::<AsyncPgConnection>::new(database_url);
Pool::builder(config).build()?
});
fn main() {
route(
"/",
get(|| async { html!(@for todo in get_todos().await {(todo)}) })
.patch(toggle_todo)
.put(add_todo)
.delete(delete_todo),
)
.wrap_non_htmx(page)
.run()
}
async fn get_todos() -> Vec<Todo> {
let mut con = DB_POOL.get().await.unwrap();
todos
.select(Todo::as_select())
.load(&mut con)
.await
.expect("successful select query")
}
async fn toggle_todo(Vals(todo): Vals<Todo>) -> Markup {
let mut con = DB_POOL.get().await.unwrap();
diesel::update(todos.find(todo.uuid))
.set(done.eq(!todo.done))
.returning(Todo::as_returning())
.get_result(&mut con)
.await
.expect("successful update query")
.render()
}
async fn add_todo(Vals(todo): Vals<Todo>) -> Markup {
let mut con = DB_POOL.get().await.unwrap();
diesel::insert_into(todos)
.values(&todo)
.returning(Todo::as_returning())
.get_result(&mut con)
.await
.expect("successful insert query")
.render()
}
async fn delete_todo(Vals(todo): Vals<Todo>) {
let mut con = DB_POOL.get().await.unwrap();
diesel::delete(todos.filter(uuid.eq(todo.uuid)))
.execute(&mut con)
.await
.expect("successful delete query");
}
impl Render for Todo {
fn render(&self) -> Markup {
html! {
$"flex items-center" into="this" swap-full vals=(json!(self)) {
input type="checkbox" patch="/" checked[self.done] {}
label $"ml-4 text-lg" {(self.task)}
button $"ml-auto" detele="/" {"Delete"}
}
}
}
}
async fn page(content: Markup) -> Markup {
html! { html { (Head::with_title("With Diesel Postgres"))
body $"max-w-screen-sm mx-auto mt-12" {
form $"flex gap-4 justify-center" put="/" into="#list" swap-beforeend after-request="this.reset()" {
input $"border rounded-md" type="text" name="task" {}
button type="submit" {"Add"}
}
div #"list" $"w-full" {(content)}
(Scripts::default())
}
}}
}