Todo App Example¶
A classic todo list application demonstrating ORMDB fundamentals.
Overview¶
This example builds a complete todo list with: - Create, read, update, delete operations - Filtering by status - Sorting by date/priority - Simple REST API
Schema¶
// schema.ormdb
entity Todo {
id: uuid @id @default(uuid())
title: string
description: string?
completed: bool @default(false)
priority: int32 @default(0)
due_date: timestamp?
created_at: timestamp @default(now())
updated_at: timestamp @default(now())
@index(completed)
@index(priority)
}
Project Setup¶
Cargo.toml¶
[package]
name = "todo-app"
version = "0.1.0"
edition = "2021"
[dependencies]
ormdb-core = "0.1"
ormdb-proto = "0.1"
tokio = { version = "1", features = ["full"] }
uuid = { version = "1", features = ["v4"] }
chrono = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
axum = "0.7"
Database Setup¶
// src/db.rs
use ormdb_core::{Database, StorageConfig};
use std::sync::Arc;
pub type Db = Arc<Database>;
pub async fn init_database() -> Db {
let config = StorageConfig::default()
.path("./data/todos.db");
let db = Database::open(config)
.await
.expect("Failed to open database");
// Apply schema
db.apply_schema(include_str!("../schema.ormdb"))
.await
.expect("Failed to apply schema");
Arc::new(db)
}
Models¶
// src/models.rs
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Todo {
pub id: Uuid,
pub title: String,
pub description: Option<String>,
pub completed: bool,
pub priority: i32,
pub due_date: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Deserialize)]
pub struct CreateTodo {
pub title: String,
pub description: Option<String>,
pub priority: Option<i32>,
pub due_date: Option<DateTime<Utc>>,
}
#[derive(Debug, Deserialize)]
pub struct UpdateTodo {
pub title: Option<String>,
pub description: Option<String>,
pub completed: Option<bool>,
pub priority: Option<i32>,
pub due_date: Option<DateTime<Utc>>,
}
#[derive(Debug, Deserialize)]
pub struct TodoFilter {
pub completed: Option<bool>,
pub priority_gte: Option<i32>,
pub sort_by: Option<String>,
pub sort_order: Option<String>,
}
Query Functions¶
// src/queries.rs
use crate::db::Db;
use crate::models::{CreateTodo, Todo, TodoFilter, UpdateTodo};
use ormdb_proto::{FilterExpr, GraphQuery, Mutation, OrderSpec, Value};
use uuid::Uuid;
/// List all todos with optional filtering
pub async fn list_todos(db: &Db, filter: TodoFilter) -> Vec<Todo> {
let mut query = GraphQuery::new("Todo");
// Apply filters
if let Some(completed) = filter.completed {
query = query.filter(FilterExpr::eq("completed", Value::Bool(completed)));
}
if let Some(priority) = filter.priority_gte {
query = query.filter(FilterExpr::gte("priority", Value::Int32(priority)));
}
// Apply sorting
if let Some(sort_by) = filter.sort_by {
let direction = match filter.sort_order.as_deref() {
Some("desc") => ormdb_proto::SortDirection::Desc,
_ => ormdb_proto::SortDirection::Asc,
};
query = query.order_by(OrderSpec::new(&sort_by, direction));
} else {
// Default: newest first
query = query.order_by(OrderSpec::desc("created_at"));
}
let result = db.query(query).await.expect("Query failed");
result.entities().map(|e| e.into()).collect()
}
/// Get a single todo by ID
pub async fn get_todo(db: &Db, id: Uuid) -> Option<Todo> {
let query = GraphQuery::new("Todo")
.filter(FilterExpr::eq("id", Value::Uuid(id.into_bytes())));
let result = db.query(query).await.expect("Query failed");
result.entities().next().map(|e| e.into())
}
/// Create a new todo
pub async fn create_todo(db: &Db, input: CreateTodo) -> Todo {
let id = Uuid::new_v4();
let mutation = Mutation::create("Todo")
.set("id", Value::Uuid(id.into_bytes()))
.set("title", Value::String(input.title))
.set_opt("description", input.description.map(Value::String))
.set("priority", Value::Int32(input.priority.unwrap_or(0)))
.set_opt("due_date", input.due_date.map(|d| Value::Timestamp(d.timestamp_micros())));
db.mutate(mutation).await.expect("Mutation failed");
get_todo(db, id).await.expect("Todo not found after create")
}
/// Update an existing todo
pub async fn update_todo(db: &Db, id: Uuid, input: UpdateTodo) -> Option<Todo> {
// Check if exists
if get_todo(db, id).await.is_none() {
return None;
}
let mut mutation = Mutation::update("Todo")
.filter(FilterExpr::eq("id", Value::Uuid(id.into_bytes())));
if let Some(title) = input.title {
mutation = mutation.set("title", Value::String(title));
}
if let Some(description) = input.description {
mutation = mutation.set("description", Value::String(description));
}
if let Some(completed) = input.completed {
mutation = mutation.set("completed", Value::Bool(completed));
}
if let Some(priority) = input.priority {
mutation = mutation.set("priority", Value::Int32(priority));
}
if let Some(due_date) = input.due_date {
mutation = mutation.set("due_date", Value::Timestamp(due_date.timestamp_micros()));
}
// Always update updated_at
mutation = mutation.set("updated_at", Value::Timestamp(chrono::Utc::now().timestamp_micros()));
db.mutate(mutation).await.expect("Mutation failed");
get_todo(db, id).await
}
/// Delete a todo
pub async fn delete_todo(db: &Db, id: Uuid) -> bool {
if get_todo(db, id).await.is_none() {
return false;
}
let mutation = Mutation::delete("Todo")
.filter(FilterExpr::eq("id", Value::Uuid(id.into_bytes())));
db.mutate(mutation).await.expect("Mutation failed");
true
}
/// Toggle todo completion status
pub async fn toggle_todo(db: &Db, id: Uuid) -> Option<Todo> {
let todo = get_todo(db, id).await?;
let mutation = Mutation::update("Todo")
.filter(FilterExpr::eq("id", Value::Uuid(id.into_bytes())))
.set("completed", Value::Bool(!todo.completed))
.set("updated_at", Value::Timestamp(chrono::Utc::now().timestamp_micros()));
db.mutate(mutation).await.expect("Mutation failed");
get_todo(db, id).await
}
/// Get todo statistics
pub async fn get_stats(db: &Db) -> TodoStats {
let all = list_todos(db, TodoFilter::default()).await;
TodoStats {
total: all.len(),
completed: all.iter().filter(|t| t.completed).count(),
pending: all.iter().filter(|t| !t.completed).count(),
high_priority: all.iter().filter(|t| t.priority >= 5).count(),
}
}
#[derive(Debug, Serialize)]
pub struct TodoStats {
pub total: usize,
pub completed: usize,
pub pending: usize,
pub high_priority: usize,
}
HTTP Handlers¶
// src/handlers.rs
use axum::{
extract::{Path, Query, State},
http::StatusCode,
Json,
};
use uuid::Uuid;
use crate::db::Db;
use crate::models::{CreateTodo, Todo, TodoFilter, UpdateTodo};
use crate::queries::{self, TodoStats};
/// GET /todos
pub async fn list_todos(
State(db): State<Db>,
Query(filter): Query<TodoFilter>,
) -> Json<Vec<Todo>> {
let todos = queries::list_todos(&db, filter).await;
Json(todos)
}
/// GET /todos/:id
pub async fn get_todo(
State(db): State<Db>,
Path(id): Path<Uuid>,
) -> Result<Json<Todo>, StatusCode> {
queries::get_todo(&db, id)
.await
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
/// POST /todos
pub async fn create_todo(
State(db): State<Db>,
Json(input): Json<CreateTodo>,
) -> (StatusCode, Json<Todo>) {
let todo = queries::create_todo(&db, input).await;
(StatusCode::CREATED, Json(todo))
}
/// PUT /todos/:id
pub async fn update_todo(
State(db): State<Db>,
Path(id): Path<Uuid>,
Json(input): Json<UpdateTodo>,
) -> Result<Json<Todo>, StatusCode> {
queries::update_todo(&db, id, input)
.await
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
/// DELETE /todos/:id
pub async fn delete_todo(
State(db): State<Db>,
Path(id): Path<Uuid>,
) -> StatusCode {
if queries::delete_todo(&db, id).await {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
/// POST /todos/:id/toggle
pub async fn toggle_todo(
State(db): State<Db>,
Path(id): Path<Uuid>,
) -> Result<Json<Todo>, StatusCode> {
queries::toggle_todo(&db, id)
.await
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
/// GET /todos/stats
pub async fn get_stats(State(db): State<Db>) -> Json<TodoStats> {
let stats = queries::get_stats(&db).await;
Json(stats)
}
Main Application¶
// src/main.rs
mod db;
mod handlers;
mod models;
mod queries;
use axum::{
routing::{delete, get, post, put},
Router,
};
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
// Initialize database
let db = db::init_database().await;
// Build router
let app = Router::new()
.route("/todos", get(handlers::list_todos).post(handlers::create_todo))
.route("/todos/stats", get(handlers::get_stats))
.route(
"/todos/:id",
get(handlers::get_todo)
.put(handlers::update_todo)
.delete(handlers::delete_todo),
)
.route("/todos/:id/toggle", post(handlers::toggle_todo))
.with_state(db);
// Run server
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Server running at http://{}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
API Usage¶
Create a Todo¶
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{
"title": "Learn ORMDB",
"description": "Read the documentation",
"priority": 5
}'
List All Todos¶
# All todos
curl http://localhost:3000/todos
# Only pending
curl "http://localhost:3000/todos?completed=false"
# High priority, sorted
curl "http://localhost:3000/todos?priority_gte=5&sort_by=priority&sort_order=desc"
Update a Todo¶
curl -X PUT http://localhost:3000/todos/550e8400-e29b-41d4-a716-446655440000 \
-H "Content-Type: application/json" \
-d '{"completed": true}'
Toggle Completion¶
Delete a Todo¶
Get Statistics¶
Response:
Key Takeaways¶
- Schema-first design - Define your data model in
.ormdbfiles - Type-safe queries - Use
GraphQueryandFilterExprinstead of SQL strings - Automatic indexing - Declare
@indexfor frequently filtered fields - Simple CRUD -
Mutation::create,update,deletecover all basics - Flexible filtering - Combine filters with AND/OR logic
Next Steps¶
- Add authentication with Security
- Implement pagination with Pagination Guide
- Try the more complex Blog Platform example