Rust Axum Web Framework Tutorial
A practical introduction to building HTTP services in Rust using the Axum web framework, with routing, extractors, state, and JSON handling.
What you'll learn
- ✓Core concept introduced
- ✓How the API is structured
- ✓Typical idiomatic usage
- ✓Common pitfalls to avoid
- ✓When and where to apply it
Prerequisites
- •Basic Rust familiarity
What and Why
Axum is a web framework for Rust built on top of tokio, hyper, and tower. It uses Rust’s type system to make routing and request handling feel ergonomic without sacrificing performance. Unlike older Rust frameworks that relied on heavy macros, Axum models handlers as plain async functions whose parameters are “extractors” - types that know how to pull data out of a request.
The “why” is straightforward. You get the speed and safety of Rust, native async I/O via Tokio, and a middleware ecosystem (the entire tower stack) shared with other Rust services like gRPC servers. That means your authentication layer, retries, timeouts, and tracing can be reused across protocols.
Mental Model
Axum has three core abstractions you should hold in your head: a Router that maps paths and methods to handlers, handlers that are async functions whose arguments implement FromRequestParts or FromRequest, and a return type that implements IntoResponse. Shared application data flows through State<T>, while request-scoped data flows through extractors like Path, Query, Json, and Extension.
If you think of a handler as (extracted inputs) -> impl IntoResponse, most of Axum clicks into place. The compiler enforces correctness: if you ask for Json<MyDto>, deserialization happens before your code runs, and a deserialization failure becomes a 400 response automatically.
Hands-on Example
Here is a minimal Axum app that exposes a JSON endpoint and uses shared state.
use axum::{extract::State, routing::get, Json, Router};
use serde::Serialize;
use std::sync::Arc;
#[derive(Clone)]
struct AppState { greeting: String }
#[derive(Serialize)]
struct Hello { message: String }
async fn hello(State(s): State<Arc<AppState>>) -> Json<Hello> {
Json(Hello { message: s.greeting.clone() })
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState { greeting: "hi".into() });
let app = Router::new()
.route("/hello", get(hello))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
The diagram shows how a byte stream becomes a typed handler call and turns back into bytes on the way out.
Common Pitfalls
The biggest pitfall newcomers hit is extractor ordering. Extractors that consume the body (like Json or Form) must come last, because only one extractor can read the body. Putting Json<T> before Path<U> will fail to compile with a confusing error.
Another trap is State versus Extension. State is type-checked at compile time and tied to the router; Extension is dynamic and resolved at runtime. Prefer State unless you genuinely need runtime-injected dependencies.
Finally, watch your error types. Returning Result<T, E> only works when E: IntoResponse. Many devs wrap errors in a custom AppError newtype to satisfy that bound.
Practical Tips
Use tower-http for tracing, CORS, and compression instead of writing your own middleware. Add #[tokio::main(flavor = "multi_thread")] for CPU-bound workloads, but the default is fine for I/O. When testing, build a Router and call it directly with tower::ServiceExt::oneshot - no network needed. Group routes with Router::nest to keep large apps tidy, and prefer axum::extract::FromRequestParts for custom extractors that don’t consume the body.
Wrap-up
Axum gives Rust developers a clean, type-driven path to writing web services. Lean on extractors, return impl IntoResponse, share state through State<T>, and reach for tower middleware before writing your own. Once the mental model clicks, you’ll find Axum apps surprisingly small and pleasant to maintain.
Related articles
- Rust Rust Actix vs Axum Comparison
A side-by-side comparison of Actix Web and Axum, covering architecture, ergonomics, performance, ecosystem, and how to pick the right one for your project.
- Rust Rust async/await and Futures Explained
How Rust's async/await desugars into a state machine, what a Future actually is, and the runtime model that makes it efficient on real workloads.
- Rust Rust Tokio Runtime Explained
How Tokio schedules tasks, manages workers, and integrates with the OS to power asynchronous Rust programs efficiently across cores.
- Rust Rust Builder Pattern Explained
Learn the builder pattern in Rust, why it fits the language so well, and how to use it for ergonomic, type-safe configuration of complex structs.