Route Organization
Nested Routes (Recommended)
pub fn build(db: PgPool) -> Router {
let user_routes = Router::new()
.route("/", get(list_users))
.route("/:id", get(get_user))
.route("/", post(create_user));
let item_routes = Router::new()
.route("/", get(list_items))
.route("/:id", get(get_item))
.route("/", post(create_item));
Router::new()
.route("/health", get(health))
.nest("/users", user_routes)
.nest("/items", item_routes)
.with_state(db)
}
Request Patterns
JSON Body
#[derive(Deserialize)]
pub struct CreateItemRequest {
pub name: String,
pub description: Option<String>,
}
pub async fn create_item(
Json(req): Json<CreateItemRequest>,
) -> Result<Json<ItemResponse>, AppError> {
// req.name is guaranteed to be String
todo!()
}
Path Parameters
pub async fn get_item(
Path(id): Path<u32>,
) -> Result<Json<ItemResponse>, AppError> {
todo!()
}
Query Parameters
#[derive(Deserialize)]
pub struct ListQuery {
pub skip: Option<u32>,
pub take: Option<u32>,
}
pub async fn list_items(
Query(q): Query<ListQuery>,
) -> Result<Json<Vec<ItemResponse>>, AppError> {
let skip = q.skip.unwrap_or(0);
let take = q.take.unwrap_or(10).min(100);
todo!()
}
// Usage: /api/items?skip=10&take=20
Error Handling
Always return typed errors that convert to HTTP responses:
pub enum AppError {
NotFound(String),
Unauthorized,
BadRequest(String),
Internal(String),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized".into()),
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
AppError::Internal(err) => (StatusCode::INTERNAL_SERVER_ERROR, err),
};
(status, Json(json!({ "error": message }))).into_response()
}
}
Usage:
pub async fn get_item(
Path(id): Path<u32>,
) -> Result<Json<ItemResponse>, AppError> {
if id == 0 {
return Err(AppError::BadRequest("ID cannot be 0".into()));
}
let item = find_item(id)
.ok_or_else(|| AppError::NotFound("Item not found".into()))?;
Ok(Json(item))
}
Validation
Serde Validation
Serde automatically validates types:
#[derive(Deserialize)]
pub struct CreateItemRequest {
pub name: String, // Guaranteed to be String or 400 Bad Request
pub price: f64, // Guaranteed to be number or 400 Bad Request
}
Custom Validation
#[derive(Deserialize)]
pub struct CreateItemRequest {
pub name: String,
pub email: String,
}
impl CreateItemRequest {
pub fn validate(&self) -> Result<(), AppError> {
if self.name.is_empty() || self.name.len() > 255 {
return Err(AppError::BadRequest("Name must be 1-255 chars".into()));
}
if !self.email.contains('@') {
return Err(AppError::BadRequest("Invalid email".into()));
}
Ok(())
}
}
pub async fn create_item(
Json(req): Json<CreateItemRequest>,
) -> Result<Json<ItemResponse>, AppError> {
req.validate()?;
todo!()
}
Pagination
#[derive(Deserialize)]
pub struct PaginationQuery {
pub skip: u32,
pub take: u32,
}
pub async fn list_items(
State(db): State<PgPool>,
Query(q): Query<PaginationQuery>,
) -> Result<Json<Vec<ItemResponse>>, AppError> {
let skip = q.skip.min(1_000_000);
let take = q.take.max(1).min(100); // Clamp between 1-100
let items = sqlx::query_as::<_, ItemResponse>(
"SELECT * FROM items LIMIT $1 OFFSET $2"
)
.bind(take)
.bind(skip)
.fetch_all(&db)
.await?;
Ok(Json(items))
}
Soft Deletes
Instead of deleting, mark as deleted:
pub async fn delete_item(
State(db): State<PgPool>,
Path(id): Path<u32>,
) -> Result<StatusCode, AppError> {
sqlx::query(
"UPDATE items SET deleted_at = NOW() WHERE id = $1"
)
.bind(id)
.execute(&db)
.await?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn list_items(
State(db): State<PgPool>,
) -> Result<Json<Vec<ItemResponse>>, AppError> {
let items = sqlx::query_as::<_, ItemResponse>(
"SELECT * FROM items WHERE deleted_at IS NULL"
)
.fetch_all(&db)
.await?;
Ok(Json(items))
}
Middleware
Logging
use tower_http::trace::TraceLayer;
Router::new()
.route("/items", get(list_items))
.layer(TraceLayer::new_for_http())
Authentication
pub async fn protected_route(
AuthUser(claims): AuthUser,
) -> String {
format!("Hello, {}", claims.sub)
}
Learn More
- Axum Documentation
- Your First Endpoint — Step-by-step tutorial
- Testing Guide — Test your endpoints
- Security — Secure API patterns