The Tokio team released Axum in July 2021 because they were sick of web frameworks that made simple shit complicated. After years of maintaining the async runtime that powers half the Rust ecosystem, they knew exactly what sucked about existing options and built something that actually works.
Why This Framework Doesn't Suck
Most web frameworks try to be clever. Axum tries to be simple. It gives you routing that compiles to normal function calls, extractors that validate your data, and middleware that doesn't mysteriously break. Everything builds on the mature Tower ecosystem instead of reinventing abstractions that already work.
Your handlers compile to normal Rust code: No reflection magic, no runtime type discovery, no mysterious performance cliffs. When Diesel's compile-time SQL verification catches your query errors and Axum's extractors validate request data, you know your code won't randomly fail in production.
Designed for Tokio from day one: Other frameworks bolt async on top of sync designs and wonder why everything deadlocks. Axum was built specifically for Tokio's async runtime, so you can handle thousands of connections without the server melting down.
The type system prevents your fuckups: When your handler compiles, it will get the right data types and won't panic on malformed requests. The error handling forces you to think about failure cases instead of discovering them in production.
Who's Actually Using This In Production
But enough theory - here's who's actually betting their production systems on this shit.
Version 0.8.4 has been running our stuff for 8 months without exploding. Vector uses Axum for its observability pipeline that processes terabytes of logs daily. Tremor runs Axum in their event processing engine. Unlike Node.js deployments that mysteriously consume all your RAM, these systems run for months without restart.
The Shit That Actually Matters
No macro DSL bullshit: Other frameworks make you learn their special syntax hidden in macros. Axum uses normal Rust functions. Your IDE provides completions, error messages point to your actual code, and you don't spend an hour waiting for incremental compilation to maybe work. No 15-minute compile times for a one-line change.
Request data that doesn't break: Function parameters automatically extract and validate data from requests:
use axum::{extract::{Path, Query}, Json, http::StatusCode};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct UserParams {
page: u32,
limit: u32,
// TODO: add validation for limit > 100
}
#[derive(Serialize)]
struct User {
id: u64,
username: String,
// Real production code has way more fields
}
async fn get_user(
Path(user_id): Path<u64>,
Query(params): Query<UserParams>,
) -> Result<Json<User>, StatusCode> {
// If extraction fails, you get proper 400 errors automatically
// No more \"req.params is undefined\" runtime surprises
if params.limit > 100 {
return Err(StatusCode::BAD_REQUEST);
}
Ok(Json(User {
id: user_id,
username: format!(\"user_{}\", user_id) // Less fake than \"example\"
}))
}
Middleware that doesn't mysteriously break: Instead of reinventing middleware abstractions, Axum uses the Tower ecosystem that's been debugged for years:
- Request tracing that actually shows useful information
- CORS headers that work with your frontend framework
- Rate limiting and timeouts that prevent abuse
- Compression that doesn't corrupt binary responses
State management that makes sense: No global variables, no dependency injection hell, just pass data where you need it:
#[derive(Clone)]
struct AppState {
db: PgPool,
redis: RedisPool,
// In production this has config, auth keys, etc.
}
async fn handler(State(state): State<AppState>) -> String {
let active_connections = state.db.num_idle();
format!(\"DB pool has {} idle connections\", active_connections)
}
No XML config bullshit, no annotations - just pass data where you need it. The dependency injection pattern that works without runtime reflection magic.