Recipe: Rust axum handler scaffold
Drop-in pattern for a typed JSON handler with shared state, request extraction, and error mapping.
Cargo.toml
[dependencies]
axum = "0.7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"main.rs
use axum::{Router, routing::post, extract::State, Json};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
db_pool: sqlx::PgPool,
}
#[derive(Deserialize)]
struct CreateUser {
email: String,
}
#[derive(Serialize)]
struct UserResponse {
id: String,
email: String,
}
async fn create_user(
State(state): State<Arc<AppState>>,
Json(payload): Json<CreateUser>,
) -> Result<Json<UserResponse>, (StatusCode, String)> {
let row = sqlx::query_as!(UserRow, "INSERT INTO users (email) VALUES ($1) RETURNING id, email", payload.email)
.fetch_one(&state.db_pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(UserResponse { id: row.id.to_string(), email: row.email }))
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let state = Arc::new(AppState { db_pool: setup_db().await });
let app = Router::new()
.route("/users", post(create_user))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}Notes
- · State extractor requires Clone — wrap in Arc for cheap sharing.
- · Return Result<Json<T>, (StatusCode, String)> for clean error mapping.
- · axum 0.7 uses axum::serve instead of the older Server::bind.