Axum & Rust Web Frameworks
Axum is the web framework maintained by the Tokio team. It has become the default choice for HTTP services in Rust for one main reason: it is deliberately small. Routing and extractors are Axum's; everything else — the runtime, the HTTP implementation, middleware, timeouts, tracing, compression — comes from tokio, hyper, and tower, which are the same crates the rest of the ecosystem already uses.
The payoff is a service that compiles to a single static binary with a ~10 MB memory floor, no GC pauses, and predictable tail latency. The cost is compile times and a type system that will argue with you until the handler is correct. This page covers the parts you actually use in production and how Axum stacks up against Actix Web and Rocket.
TL;DR
- Axum is Tokio + Hyper + Tower, with routing and extractors on top — no bespoke middleware or runtime of its own.
- Handlers are plain async functions; arguments are extractors (
Path,Query,Json,State) that parse the request for you. - Extractor order matters: body-consuming extractors (
Json,Form,String) must be the last argument. - Shared state travels via
State<T>— typically anArcof a struct holding a connection pool and config. - Middleware is Tower
Layers, so you inherittower-http(tracing, CORS, compression, timeouts) and anything else Tower-compatible. - Errors work by returning
Result<T, E>whereE: IntoResponse— no exceptions, no panics as control flow. - Pick Axum for ecosystem alignment, Actix Web for peak benchmark throughput, Rocket for ergonomics-first simplicity.
Quick Example
A complete service with routing, JSON, path params, and shared state:
Core Concepts
Handlers are async functions
Any async fn whose arguments implement FromRequestParts/FromRequest and whose return type implements IntoResponse is a valid handler. There is no handler trait to implement and no macro to apply.
When a handler doesn't compile, the error is often a wall of trait bounds. The cause is nearly always one of: an argument that isn't an extractor, a return type that isn't IntoResponse, or a non-Send value held across an .await. The #[axum::debug_handler] macro turns that wall into a readable message — reach for it immediately.
Extractors
Extractors are typed request parsing. Each one either succeeds or short-circuits with its own rejection response.
Option<Json<T>> makes an extractor optional, and Result<Json<T>, JsonRejection> gives you the parse error to shape yourself:
⚠️ Warning: The body can only be consumed once, so exactly one body extractor is allowed and it must be the final argument.
async fn h(Json(b): Json<T>, State(s): State<S>)will not compile — the diagnostic is opaque, the fix is to swap the order.
Shared state
State<T> is the type-checked way to hand handlers a connection pool, an HTTP client, or config. Wrap in Arc so cloning per request is a refcount bump.
Nested routers each carry their own state type, which is why a merged router with mismatched state fails to compile. Router::nest with with_state applied at the right level resolves it.
Responses via IntoResponse
Anything convertible to a response can be returned. Tuples let you layer status and headers:
For heterogeneous return paths, return Response (i.e. axum::response::Response) and call .into_response() on each branch, or define an error enum.
Middleware with Tower
Axum has no middleware system of its own — it uses Tower's Service/Layer abstraction. That means the middleware ecosystem is shared with every other Tower-based project.
Layers apply bottom-up: the last one added is closest to the handler. Getting this backwards is why a tracing layer sometimes misses requests rejected by an auth layer.
Custom middleware
For request-scoped work like authentication, middleware::from_fn is the pragmatic option:
Use route_layer (not layer) for middleware that should only guard the routes declared before it. See JWT and Authentication.
Error Handling
The idiomatic pattern is one application error enum implementing IntoResponse, so handlers can use ? freely.
This is the whole story: #[from] conversions plus ?, with the mapping to HTTP status codes centralized in one impl. See Error Handling.
The Rust Web Stack
Axum is one layer of a stack you assemble yourself:
Comparison
Framework choice rarely decides your performance — all four sit in the same top tier of HTTP benchmarks, and your database will be the bottleneck long before the router is. Choose on ecosystem fit and error-message tolerance.
Why pick Rust here at all?
Against Express, FastAPI, or Spring Boot, Rust buys you: a single static binary (tiny container images, fast cold starts — relevant for serverless), no GC and therefore stable p99s, memory footprints measured in single-digit MB, and compile-time guarantees that eliminate a class of runtime errors. It costs you: slow builds, a steeper hiring bar, and a thinner library ecosystem for niche integrations. It's a strong fit for latency-sensitive services, high-throughput proxies and gateways, and CPU-bound work; a weaker one for CRUD apps where developer velocity dominates.
Best Practices
Return Result with a typed error, never .unwrap()
A panic in a handler aborts that connection and pollutes your logs with backtraces. Model failures in the type system and let IntoResponse map them.
Keep the body extractor last, and use debug_handler while iterating
Never block the async runtime
Blocking a Tokio worker thread stalls every task scheduled on it. Offload CPU-heavy or blocking-IO work explicitly:
Use tracing with spans, not println!
TraceLayer plus #[instrument] gives you per-request spans that slot straight into distributed tracing and OpenTelemetry.
Enforce graceful shutdown
In-flight requests finish instead of being severed mid-response during a rolling deploy.
Build slim images with a multi-stage Dockerfile
A distroless image with one static binary is typically 20–30 MB and has almost no CVE surface. See Docker and Container Security.
Common Mistakes
Body extractor in the wrong position
Creating clients or pools per request
Panicking on bad input
Leaking internal errors to clients
Blocking inside async
FAQ
Should I use Axum or Actix Web in 2026?
Axum is the safer default: it is maintained by the Tokio team, and its middleware is plain Tower, so anything you build is reusable outside Axum. Choose Actix Web if you have benchmarked evidence that its throughput edge matters for your workload, or an existing team fluent in it.
Why are my handler compile errors so unreadable?
Because handler validity is expressed through trait bounds, so a small mismatch surfaces as a long unsatisfied-bound chain. Add #[axum::debug_handler] to the handler — it reports the actual problem (wrong extractor order, non-Send value, missing IntoResponse) in one line.
Is Axum production-ready?
Yes. It's the HTTP layer for a wide range of production Rust services and is versioned deliberately (0.x releases have contained breaking changes — pin your version and read the changelog before upgrading).
How do I serve a frontend alongside the API?
Use tower-http's ServeDir with a SPA fallback, or put a CDN in front and keep the Rust service API-only. The latter is usually better — see CDN.
Does Axum support WebSockets and SSE?
Both, natively: axum::extract::ws::WebSocketUpgrade and axum::response::Sse. Rust's low per-connection memory makes it a strong fit for holding many concurrent connections — see WebSockets and Server-Sent Events.
What ORM should I pair with it?
sqlx if you want SQL with compile-time query verification (the common choice), sea-orm if you want an async ORM with relations, diesel if you want the most mature schema/migration tooling. See Object-Relational Mapping.
Related Topics
- Rust — ownership, async, and the language fundamentals Axum leans on
- REST API — the API design Axum routes implement
- Express — the Node equivalent, for comparison
- FastAPI — Python's extractor-style framework
- Error Handling — patterns behind the
IntoResponseerror enum - Object-Relational Mapping — sqlx, SeaORM, Diesel in context
- WebSockets · Server-Sent Events — real-time endpoints in Axum
- Docker — multi-stage builds for tiny Rust images
- OpenTelemetry — wiring
tracingspans to your observability stack - Systems Programming — why Rust's guarantees matter at this layer