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
txt
basic/ Cargo.toml src/ main.rsThat's the entire project. The .db file is created on first run.
Cargo.toml
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
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
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, noState<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(). Thenotemodule is generated by#[derive(Model)]. - Zero serializer code.
#[derive(Serialize)]on the struct is enough; the handler returnsJson<Vec<Note>>directly.
Next
- Add an HTML route: see the Your first app walkthrough.
- Add CRUD without writing handlers: see Batteries included.
- Turn this into a REST microservice: see REST API service.
examplesgetting-started