Axum is the Rust web framework from the Tokio team — ergonomic, type-safe, and blazingly fast. It handles millions of requests per second on a single machine.
Why Axum?
- By Tokio team — first-class async runtime integration
- Type-safe extractors — request parsing that can't fail silently
- Tower middleware — entire Tower ecosystem of middleware
- No macros — uses regular Rust functions, not magic annotations
- Shared state — Arc-based state sharing, no hidden magic
- WebSocket support — built in, fully typed
Quick Start
# Cargo.toml
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use axum::{routing::get, Router, Json};
use serde::Serialize;
#[derive(Serialize)]
struct Health {
status: String,
version: String,
}
async fn health() -> Json<Health> {
Json(Health {
status: "ok".to_string(),
version: "1.0.0".to_string(),
})
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/health", get(health));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Extractors (Type-Safe Request Parsing)
use axum::{
extract::{Path, Query, State, Json},
http::StatusCode,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
limit: Option<u32>,
}
// Path parameters
async fn get_user(Path(id): Path<u64>) -> String {
format!("User {}", id)
}
// Query parameters
async fn list_users(Query(params): Query<Pagination>) -> String {
let page = params.page.unwrap_or(1);
let limit = params.limit.unwrap_or(10);
format!("Page {}, Limit {}", page, limit)
}
// JSON body
async fn create_user(Json(payload): Json<CreateUser>) -> (StatusCode, Json<serde_json::Value>) {
(
StatusCode::CREATED,
Json(serde_json::json!({
"name": payload.name,
"email": payload.email,
})),
)
}
Shared State
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
struct AppState {
db: Arc<Pool<Postgres>>,
cache: Arc<RwLock<HashMap<String, String>>>,
}
async fn get_value(
State(state): State<AppState>,
Path(key): Path<String>,
) -> Result<String, StatusCode> {
let cache = state.cache.read().await;
cache.get(&key)
.cloned()
.ok_or(StatusCode::NOT_FOUND)
}
#[tokio::main]
async fn main() {
let state = AppState {
db: Arc::new(create_pool().await),
cache: Arc::new(RwLock::new(HashMap::new())),
};
let app = Router::new()
.route("/cache/:key", get(get_value))
.with_state(state);
// ...
}
Error Handling
use axum::response::{IntoResponse, Response};
enum AppError {
NotFound(String),
BadRequest(String),
Internal(anyhow::Error),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
AppError::Internal(err) => {
eprintln!("Internal error: {:?}", err);
(StatusCode::INTERNAL_SERVER_ERROR, "Internal error".to_string())
}
};
(status, Json(serde_json::json!({ "error": message }))).into_response()
}
}
async fn get_user(Path(id): Path<u64>) -> Result<Json<User>, AppError> {
let user = db::find_user(id)
.await
.map_err(AppError::Internal)?
.ok_or_else(|| AppError::NotFound(format!("User {} not found", id)))?;
Ok(Json(user))
}
Middleware (Tower)
use axum::middleware;
use tower_http::{
cors::CorsLayer,
compression::CompressionLayer,
trace::TraceLayer,
timeout::TimeoutLayer,
};
let app = Router::new()
.route("/api/users", get(list_users).post(create_user))
.route("/api/users/:id", get(get_user))
.layer(CorsLayer::permissive())
.layer(CompressionLayer::new())
.layer(TraceLayer::new_for_http())
.layer(TimeoutLayer::new(Duration::from_secs(30)));
WebSockets
use axum::extract::ws::{WebSocket, WebSocketUpgrade, Message};
async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
ws.on_upgrade(handle_socket)
}
async fn handle_socket(mut socket: WebSocket) {
while let Some(Ok(msg)) = socket.recv().await {
if let Message::Text(text) = msg {
let response = format!("Echo: {}", text);
if socket.send(Message::Text(response)).await.is_err() {
break;
}
}
}
}
let app = Router::new().route("/ws", get(ws_handler));
Axum vs Actix-web vs Rocket vs Express
| Feature | Axum | Actix-web | Rocket | Express |
|---|---|---|---|---|
| Speed | Fastest tier | Fastest tier | Fast | Medium |
| Memory | ~2MB | ~5MB | ~10MB | ~50MB |
| Async | Tokio native | Custom runtime | Tokio | libuv |
| Type safety | Compile-time | Compile-time | Compile-time | Runtime |
| Middleware | Tower (composable) | Custom | Fairings | Connect |
| WebSocket | Built-in | Built-in | No | ws package |
| Learning curve | Medium | Medium | Low | Low |
Need to scrape data from any website and get it in structured JSON? Check out my web scraping tools on Apify — no coding required, results in minutes.
Have a custom data extraction project? Email me at spinov001@gmail.com — I build tailored scraping solutions for businesses.
Top comments (0)