This version is in beta. Some features may change before release.

Basic

The smallest umbral app. One model, one route, default settings. Copy-paste, cargo run.

The smallest umbral app that actually does something. One model, one route, default file-backed SQLite, auto-migrate on boot. Nothing else. The point is to show the shape; everything else is opt-in.

Project layout

Code
txt
basic/
Cargo.toml
src/
main.rs

That's the entire project. The .db file is created on first run.

Cargo.toml

Code
toml
[package]
name = "basic"
version = "0.1.0"
edition = "2024"
 
[dependencies]
umbral = "0.0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }

src/main.rs

Code
rust
use umbral::prelude::*;
use umbral::web::Json;
 
#[derive(Debug, Clone, serde::Serialize, sqlx::FromRow, Model)]
pub struct Note {
pub id: i64,
pub title: String,
pub body: String,
pub created_at: chrono::DateTime<chrono::Utc>,
}
 
async fn list_notes() -> Result<Json<Vec<Note>>, (umbral::web::StatusCode, String)> {
let notes = Note::objects()
.order_by(note::ID.desc())
.limit(50)
.fetch()
.await
.map_err(|e| (umbral::web::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(notes))
}
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let settings = Settings::from_env()?;
let pool = umbral::db::connect("sqlite://basic.db?mode=rwc").await?;
 
let app = App::builder()
.settings(settings)
.database("default", pool)
.model::<Note>()
.routes(Routes::new().get("/notes", list_notes))
.build()?;
 
// Auto-migrate on boot. Demo-only. Production splits this into
// `cargo run -- makemigrations` and `cargo run -- migrate`.
umbral::migrate::make().await.ok();
umbral::migrate::run().await?;
 
let addr = "127.0.0.1:3000".parse::<std::net::SocketAddr>()?;
println!("listening on http://{addr}");
app.serve(addr).await?;
Ok(())
}

Run it

Code
bash
cargo run
# listening on http://127.0.0.1:3000
 
# Seed a row directly via sqlite (or write a `create_note` POST handler):
echo "INSERT INTO note (title, body, created_at) VALUES ('hello', 'first note', datetime('now'));" \
| sqlite3 basic.db
 
curl http://127.0.0.1:3000/notes
# [{"id":1,"title":"hello","body":"first note","created_at":"…"}]

What this example shows

  • One-line ORM access. Note::objects().fetch() works from any handler, no State<DbPool> extractor.
  • Managed migrations. umbral::migrate::make() diffs the model against the snapshot; run() applies pending migrations.
  • Compile-time-checked column constants. note::ID.desc(). The note module is generated by #[derive(Model)].
  • Zero serializer code. #[derive(Serialize)] on the struct is enough; the handler returns Json<Vec<Note>> directly.

Next

examplesgetting-started