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.
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
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
published BOOLEAN NOT NULL DEFAULT FALSE
);
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 withserve_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
Transactionalmiddleware that wraps each request in a transaction. - Prefer sqlx, SeaORM or Diesel? Put the pool in
app.state(pool)and read it withreq.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.