DEV Community

Alex Spinov
Alex Spinov

Posted on

Axum Has a Free API — The Rust Web Framework by Tokio

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"
Enter fullscreen mode Exit fullscreen mode
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();
}
Enter fullscreen mode Exit fullscreen mode

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,
        })),
    )
}
Enter fullscreen mode Exit fullscreen mode

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);

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

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)));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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)