Rust Axum JSON Handler with Validation
Serve typed JSON from Axum 0.8 with path and query extractors, explicit 400 and 404 errors, and shared map state behind an Arc.
Code
use std::collections::HashMap;
use std::sync::Arc;
use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Json, Router,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize)]
struct Item {
id: u32,
name: String,
price_cents: u32,
}
#[derive(Debug, Deserialize)]
struct ListQuery {
limit: Option<usize>,
offset: Option<usize>,
}
#[derive(Debug, Serialize)]
struct ErrorBody {
error: String,
}
#[derive(Debug)]
enum ApiError {
NotFound(u32),
BadRequest(String),
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, message) = match self {
ApiError::NotFound(id) => (StatusCode::NOT_FOUND, format!("item {id} not found")),
ApiError::BadRequest(message) => (StatusCode::BAD_REQUEST, message),
};
(status, Json(ErrorBody { error: message })).into_response()
}
}
type AppState = Arc<HashMap<u32, Item>>;
async fn get_item(
State(items): State<AppState>,
Path(id): Path<u32>,
) -> Result<Json<Item>, ApiError> {
items
.get(&id)
.cloned()
.map(Json)
.ok_or(ApiError::NotFound(id))
}
async fn list_items(
State(items): State<AppState>,
Query(query): Query<ListQuery>,
) -> Result<Json<Vec<Item>>, ApiError> {
let limit = query.limit.unwrap_or(10);
if !(1..=50).contains(&limit) {
return Err(ApiError::BadRequest("limit must be between 1 and 50".to_string()));
}
let offset = query.offset.unwrap_or(0);
let mut all: Vec<Item> = items.values().cloned().collect();
all.sort_by_key(|item| item.id);
Ok(Json(all.into_iter().skip(offset).take(limit).collect()))
}
#[tokio::main]
async fn main() {
let mut catalog = HashMap::new();
catalog.insert(1, Item { id: 1, name: "Keyboard".to_string(), price_cents: 4_900 });
catalog.insert(2, Item { id: 2, name: "Mouse".to_string(), price_cents: 2_500 });
let app = Router::new()
.route("/items", get(list_items))
.route("/items/{id}", get(get_item))
.with_state(Arc::new(catalog));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("listening on http://{}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}Report a problem with this exampleOpens a pre-filled issue on GitHub.