You’ve heard the rumors: Rust is blazing fast and memory‑safe, but writing backend services feels intimidating. What if it could feel like building with LEGO — intuitive, composable, and fun? Enter Axum, a modern Web framework built by the Tokio team.
In this tutorial you’ll:
- Understand Axum’s core concepts: Router, handlers, extractors
- Work with path parameters, query parameters, and JSON
- Add shared state with
Arc<Mutex<T>>
- Install middleware such as structured tracing
Step 1: Add dependencies
axum = "0.7"
tokio = { version = "1", features = ["full"] }
Or via CLI:
cargo add axum
cargo add tokio --features full
Step 2: Your first Axum app
use axum::{routing::get, Router};
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(root_handler));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("🚀 Listening on {addr}");
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn root_handler() -> &'static str {
"Hello from Axum!"
}
Step 3: Path parameters
use axum::{extract::Path, routing::get, Router};
let app = Router::new()
.route("/", get(root_handler))
.route("/hello/:name", get(greet));
async fn greet(Path(name): Path<String>) -> String {
format!("👋 Hello, {name}! Nice to meet you.")
}
Step 4: Query parameters
use axum::extract::{Query, Path};
use serde::Deserialize;
#[derive(Deserialize)]
struct SearchParams {
q: String,
lang: Option<String>,
}
let app = Router::new().route("/search", get(search));
async fn search(Query(params): Query<SearchParams>) -> String {
let language = params.lang.unwrap_or_else(|| "unknown".to_string());
format!(
"🔍 Search received! q='{0}', lang='{1}'",
params.q, language
)
}
Step 5: JSON input/output
use axum::{extract::Json, response::IntoResponse};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser { username: String, email: String }
#[derive(Serialize)]
struct User { id: u64, username: String, email: String }
let app = Router::new().route("/users", axum::routing::post(create_user));
async fn create_user(Json(payload): Json<CreateUser>) -> impl IntoResponse {
let user = User { id: 1337, username: payload.username, email: payload.email };
(axum::http::StatusCode::CREATED, Json(user))
}
Step 6: Shared state with Arc<Mutex<T>>
use axum::{extract::Extension, routing::get, Router};
use std::sync::{Arc, Mutex};
type SharedState = Arc<Mutex<u32>>;
#[tokio::main]
async fn main() {
let shared_state: SharedState = Arc::new(Mutex::new(0));
let app = Router::new()
.route("/", get(root_handler))
.route("/hits", get(hit_counter))
.layer(Extension(shared_state));
}
async fn hit_counter(Extension(state): Extension<SharedState>) -> String {
let mut count = state.lock().unwrap();
*count += 1;
format!("You are visitor #{count}!")
}
Step 7: Middleware — request tracing
cargo add tower-http --features trace
use tower_http::trace::TraceLayer;
let app = Router::new()
// ... routes and layers above
.layer(TraceLayer::new_for_http());
Graduation challenge
Build a mini server with:
/count
(GET): global counter with shared state/echo?text=...
(GET): echoes thetext
query parameter/api/greet
(POST): accepts{ "name": "..." }
and returns{ "message": "Welcome, ..." }
- Global tracing middleware
Happy shipping! 🚀