Cookbook / Data and storage

A database with axeon-orm

Models, typed queries, migrations and pagination on SQLite or PostgreSQL, wired into Axeon errors.

axeon-orm is the companion ORM. Models are plain structs, queries are type-checked, and errors map to HTTP statuses: a missing row is a 404, a unique-constraint violation is a 409.

Terminal
cargo add axeon-orm --no-default-features --features sqlite,axeon
cargo add tokio --features full
cargo add serde --features derive

Migrations are plain SQL files, applied in filename order and recorded so each runs once:

migrations/20261020000000_create_posts.sql
-- migrations/20261020000000_create_posts.sql
CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    published BOOLEAN NOT NULL DEFAULT FALSE
);
src/main.rs
use axeon::{Json, Server, ServerError};
use axeon_orm::prelude::*;

#[derive(Model, serde::Serialize)]
struct Post {
    id: i64,
    title: String,
    published: bool,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let db = Db::connect("sqlite://blog.db?mode=rwc").await?;
    // Migrations are SQL files in ./migrations, compiled into the binary.
    axeon_orm::embed_migrations!().run(&db).await?;

    let mut app = Server::new();
    app.state(db);

    app.get("/posts", |req| async move {
        let page = req.query_param("page").and_then(|p| p.parse().ok()).unwrap_or(1);
        let posts = Post::query()
            .filter(Post::PUBLISHED.eq(true))
            .order_by(Post::ID.desc())
            .paginate(req.db()?, page, 20)
            .await?;
        Ok::<_, ServerError>(Json(posts))
    });

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
    app.serve_with_shutdown(listener, axeon::shutdown_signal()).await?;
    Ok(())
}

Notes#

  • Connect inside your own Tokio runtime with #[tokio::main], then serve with serve_with_shutdown(listener, axeon::shutdown_signal()). Shutdown stays graceful.
  • The ORM also supports PostgreSQL and MySQL, relations (including many-to-many), joins, aggregates, and a Transactional middleware that wraps each request in a transaction.
  • Prefer sqlx, SeaORM or Diesel? Put the pool in app.state(pool) and read it with req.state::<PgPool>(). Axeon itself depends on no database crate.
  • A complete worked example, with JWT login, validation, health checks and tests, lives in examples/todo-api.