Loading page…
Rust walkthroughs
Loading page…
Axum is an ergonomic web framework built on Tokio and Hyper, focusing on modularity and type-safe routing. It leverages Rust's type system to extract request data, handle errors, and compose handlers. Axum integrates seamlessly with the Tower ecosystem for middleware and service composition.
Core concepts:
State extractorAxum's compile-time guarantees catch routing and extraction errors before runtime.
# Cargo.toml
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"use axum::{
extract::{Path, Query},
response::{IntoResponse, Json},
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// ===== Response Types =====
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
#[derive(Debug, Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Debug, Deserialize)]
struct Pagination {
#[serde(default = "default_page")]
page: u32,
#[serde(default = "default_limit")]
limit: u32,
}
fn default_page() -> u32 { 1 }
fn default_limit() -> u32 { 10 }
// ===== Handlers =====
async fn hello() -> &'static str {
"Hello, World!"
}
async fn json_response() -> Json<User> {
Json(User {
id: 1,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
})
}
async fn path_param(Path(id): Path<u32>) -> impl IntoResponse {
format!("User ID: {}", id)
}
async fn query_params(Query(params): Query<HashMap<String, String>>) -> impl IntoResponse {
format!("Query params: {:?}", params)
}
async fn pagination(Query(pagination): Query<Pagination>) -> impl IntoResponse {
format!("Page: {}, Limit: {}", pagination.page, pagination.limit)
}
async fn create_user(Json(payload): Json<CreateUser>) -> impl IntoResponse {
let user = User {
id: 42,
name: payload.name,
email: payload.email,
};
(axum::http::StatusCode::CREATED, Json(user))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hello))
.route("/user", get(json_response))
.route("/user/{id}", get(path_param))
.route("/search", get(query_params))
.route("/items", get(pagination))
.route("/user", post(create_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}use axum::{
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Json},
routing::{delete, get, post, put},
Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
// ===== Domain Types =====
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Todo {
id: u64,
title: String,
completed: bool,
}
#[derive(Debug, Deserialize)]
struct CreateTodo {
title: String,
}
#[derive(Debug, Deserialize)]
struct UpdateTodo {
title: Option<String>,
completed: Option<bool>,
}
// ===== Application State =====
type Db = Arc<RwLock<Vec<Todo>>>;
#[derive(Clone)]
struct AppState {
db: Db,
}
// ===== Handlers =====
async fn list_todos(State(state): State<AppState>) -> impl IntoResponse {
let todos = state.db.read().await;
Json(todos.clone())
}
async fn get_todo(
State(state): State<AppState>,
Path(id): Path<u64>,
) -> Result<impl IntoResponse, StatusCode> {
let todos = state.db.read().await;
let todo = todos.iter().find(|t| t.id == id).ok_or(StatusCode::NOT_FOUND)?;
Ok(Json(todo.clone()))
}
async fn create_todo(
State(state): State<AppState>,
Json(payload): Json<CreateTodo>,
) -> impl IntoResponse {
let mut todos = state.db.write().await;
let id = todos.len() as u64 + 1;
let todo = Todo {
id,
title: payload.title,
completed: false,
};
todos.push(todo.clone());
(StatusCode::CREATED, Json(todo))
}
async fn update_todo(
State(state): State<AppState>,
Path(id): Path<u64>,
Json(payload): Json<UpdateTodo>,
) -> Result<impl IntoResponse, StatusCode> {
let mut todos = state.db.write().await;
let todo = todos.iter_mut().find(|t| t.id == id).ok_or(StatusCode::NOT_FOUND)?;
if let Some(title) = payload.title {
todo.title = title;
}
if let Some(completed) = payload.completed {
todo.completed = completed;
}
Ok(Json(todo.clone()))
}
async fn delete_todo(
State(state): State<AppState>,
Path(id): Path<u64>,
) -> impl IntoResponse {
let mut todos = state.db.write().await;
let len_before = todos.len();
todos.retain(|t| t.id != id);
if todos.len() < len_before {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
#[tokio::main]
async fn main() {
let state = AppState {
db: Arc::new(RwLock::new(Vec::new())),
};
let app = Router::new()
.route("/todos", get(list_todos).post(create_todo))
.route("/todos/{id}", get(get_todo).put(update_todo).delete(delete_todo))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}use axum::{
extract::State,
http::StatusCode,
response::{IntoResponse, Json, Response},
routing::get,
Router,
};
use serde_json::json;
use std::sync::Arc;
// ===== Error Types =====
#[derive(Debug)]
enum ApiError {
NotFound(String),
ValidationError(String),
InternalError(String),
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, message) = match self {
ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
ApiError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg),
ApiError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
};
let body = Json(json!({
"error": message,
}));
(status, body).into_response()
}
}
// ===== Result Wrapper =====
type ApiResult<T> = Result<T, ApiError>;
// ===== Handlers with Error Handling =====
async fn get_user(Path(id): Path<u32>) -> ApiResult<impl IntoResponse> {
if id == 0 {
return Err(ApiError::ValidationError("Invalid user ID".to_string()));
}
if id > 100 {
return Err(ApiError::NotFound(format!("User {} not found", id)));
}
Ok(Json(json!({
"id": id,
"name": format!("User {}", id),
})))
}
use axum::extract::Path;
// ===== Fallback Handler =====
async fn fallback() -> impl IntoResponse {
(
StatusCode::NOT_FOUND,
Json(json!({ "error": "Route not found" })),
)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/user/{id}", get(get_user))
.fallback(fallback);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}use axum::{
body::Body,
extract::Request,
http::{header, StatusCode},
middleware::{self, Next},
response::{IntoResponse, Response},
routing::get,
Router,
};
use serde_json::json;
// ===== Timing Middleware =====
async fn timing_middleware(
request: Request,
next: Next,
) -> Response {
let start = std::time::Instant::now();
let response = next.run(request).await;
let elapsed = start.elapsed();
println!("Request took {:?}", elapsed);
response
}
// ===== Auth Middleware =====
async fn auth_middleware(
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let auth_header = request
.headers()
.get(header::AUTHORIZATION)
.and_then(|h| h.to_str().ok());
match auth_header {
Some(token) if token == "Bearer secret-token" => {
Ok(next.run(request).await)
}
_ => Err(StatusCode::UNAUTHORIZED),
}
}
// ===== Handlers =====
async fn public() -> impl IntoResponse {
"Public endpoint"
}
async fn protected() -> impl IntoResponse {
"Protected endpoint - you're authenticated!"
}
#[tokio::main]
async fn main() {
let protected_routes = Router::new()
.route("/dashboard", get(protected))
.route_layer(middleware::from_fn(auth_middleware));
let app = Router::new()
.route("/", get(public))
.merge(protected_routes)
.layer(middleware::from_fn(timing_middleware));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}use axum::{
extract::{ConnectInfo, OriginalUri, State},
http::{Method, Uri},
response::IntoResponse,
routing::get,
Router,
};
use std::net::SocketAddr;
#[derive(Clone)]
struct ApiContext {
version: String,
}
async fn debug_info(
method: Method,
uri: Uri,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
State(ctx): State<ApiContext>,
) -> impl IntoResponse {
format!(
"Method: {}\nURI: {}\nClient: {}\nAPI Version: {}",
method, uri, addr, ctx.version
)
}
// Nested router for API v1
fn api_v1() -> Router<ApiContext> {
Router::new()
.route("/users", get(|| async { "v1 users" }))
.route("/items", get(|| async { "v1 items" }))
}
// Nested router for API v2
fn api_v2() -> Router<ApiContext> {
Router::new()
.route("/users", get(|| async { "v2 users" }))
.route("/items", get(|| async { "v2 items" }))
}
#[tokio::main]
async fn main() {
let ctx = ApiContext {
version: "1.0.0".to_string(),
};
let app = Router::new()
.route("/debug", get(debug_info))
.nest("/api/v1", api_v1())
.nest("/api/v2", api_v2())
.with_state(ctx);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
println!("Server running on http://{}", addr);
axum::serve(listener, app).await.unwrap();
}use axum::{
http::{header, Method},
routing::get,
Router,
};
use tower_http::{
cors::{Any, CorsLayer},
services::ServeDir,
};
#[tokio::main]
async fn main() {
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION]);
let app = Router::new()
.route("/api/hello", get(|| async { "Hello from API" }))
.nest_service("/static", ServeDir::new("./static"))
.layer(cors);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}Path, Query, Json, and State declaratively parse request data(StatusCode, impl IntoResponse) tuples for full control over status and headersRouter::new().route(path, method(handler)) to define routes.with_state(state) and extract it with State(state): State<AppState>.nest("/prefix", router) for modular API design.layer() or .route_layer() for per-route middlewareIntoResponse for custom error types to unify error handlingaxum::extract::Request for raw access to the entire HTTP requestfallback() handler catches unmatched routestower-http::cors) integrates seamlessly via .layer()ServeDir from tower-httpaxum::serve(listener, app).await for a production-ready server