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:

  1. cargo install diesel_cli --no-default-features --features postgres - install diesel CLI that you'll need for common diesel-related ops
  2. docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -d postgres - start a postgres instance in a docker container
  3. cd examples/databases/postgres-diesel && diesel setup --migration-dir="./migrations/" - setup database & migrations
  4. cargo 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 = "0.2"
serde = { version = "1", features = ["derive"] }
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::Uuid;

#[derive(Queryable, Selectable, Insertable, serde::Serialize, serde::Deserialize)]
#[diesel(table_name = crate::schema::todos)]
#[diesel(check_for_backend(Pg))]
pub struct Todo {
    #[serde(default = "Uuid::new_v4")]
    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(Form(todo): Form<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(Form(todo): Form<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(Form(todo): Form<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" hx-target="this" hx-swap="outerHTML" hx-vals=(json!(self)) {
                input."toggle toggle-primary" type="checkbox" hx-patch="/" checked[self.done] {}
                label."ml-4 text-lg" {(self.task)}
                button."btn btn-ghost ml-auto" hx-delete="/" {"Delete"}
            }
        }
    }
}

async fn page(content: Markup) -> Markup {
    html! { html data-theme="dark" {
        (Head::with_title("With Diesel Postgres"))
        body."max-w-screen-sm mx-auto mt-12" {
            form."flex gap-4 justify-center" hx-put="/" hx-target="div" hx-swap="beforeend" hx-on--after-request="this.reset()" {
                input."input input-bordered input-primary" type="text" name="task" {}
                button."btn btn-outline btn-primary" type="submit" {"Add"}
            }
            ."w-full" {(content)}
            (Scripts::default())
        }
    }}
}