Axum

Modern Rust web framework fully integrated with Tokio ecosystem. Excellent ergonomics with performance comparable to Actix-web.

RustFrameworkWeb DevelopmentAsyncTowerTokioHTTP/2

GitHub Overview

tokio-rs/axum

Ergonomic and modular web framework built with Tokio, Tower, and Hyper

Stars22,693
Watchers141
Forks1,222
Created:May 30, 2021
Language:Rust
License:MIT License

Topics

None

Star History

tokio-rs/axum Star History
Data as of: 8/13/2025, 01:43 AM

Framework

Axum

Overview

Axum is a web framework for Rust built on the Tokio, Tower, and Hyper ecosystem. It focuses on macro-free APIs and strong type safety through Rust's powerful type system.

Details

Axum was developed in 2021 by the Tokio team as a modern web framework for the Rust language. Built on the foundation of the Tokio and Tower ecosystems, it uses Hyper as the HTTP implementation to leverage industry-standard asynchronous processing infrastructure. Axum adopts a macro-free API approach, enabling type-safe web development without relying on procedural macros. It provides a robust middleware system through Tower::Service and Tower::Layer, emphasizing reusability and composability in its design. The Extractor system enables type-safe extraction of request data, allowing various request elements (paths, queries, headers, bodies, etc.) to be received as structured Rust types. Additionally, it comes standard with WebSocket support, flexible response types, JSON serialization, file serving, and error handling capabilities. Through deep integration with the Tokio ecosystem, it provides high performance and scalability, making it suitable for enterprise-level web application development.

Pros and Cons

Pros

  • Macro-free API: Clear and understandable API without relying on procedural macros
  • Tower Integration: Full compatibility with the rich Tower middleware ecosystem
  • Type Safety: Compile-time error detection through Rust's powerful type system
  • High Performance: Extremely fast async processing based on Tokio/Hyper
  • Extractor System: Type-safe request data extraction functionality
  • Ergonomic Design: Intuitive API that prioritizes developer experience
  • Active Development: Continuous improvement and support by the Tokio team

Cons

  • Learning Curve: Requires understanding of Rust language and lifetime concepts
  • Ecosystem: Limited libraries compared to other languages due to relative newness
  • Compile Time: Long compilation times characteristic of Rust
  • Type Complexity: Complexity of the type system due to advanced type safety
  • Debug Difficulty: Challenging debugging of async code and lifetimes

Key Links

Code Examples

Hello World

use axum::{
    response::Html,
    routing::get,
    Router,
};

#[tokio::main]
async fn main() {
    // Create router
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .route("/html", get(html_handler));

    // Start server
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("Server running on: http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

async fn html_handler() -> Html<&'static str> {
    Html("<h1>Hello, HTML!</h1>")
}

Routing and Path Extraction

use axum::{
    extract::{Path, Query},
    http::StatusCode,
    response::Json,
    routing::{get, post},
    Router,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
    email: String,
}

#[derive(Deserialize)]
struct Pagination {
    page: Option<usize>,
    per_page: Option<usize>,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(root))
        .route("/users/:id", get(get_user))
        .route("/users/:id/:action", post(user_action))
        .route("/search", get(search_users))
        .route("/users", post(create_user));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn root() -> &'static str {
    "Axum API Server"
}

// Path parameter extraction
async fn get_user(Path(user_id): Path<u32>) -> Json<User> {
    let user = User {
        id: user_id,
        name: format!("User {}", user_id),
        email: format!("user{}@example.com", user_id),
    };
    Json(user)
}

// Multiple path parameters
async fn user_action(
    Path((user_id, action)): Path<(u32, String)>,
) -> Result<String, StatusCode> {
    match action.as_str() {
        "activate" => Ok(format!("User {} activated", user_id)),
        "deactivate" => Ok(format!("User {} deactivated", user_id)),
        "delete" => Ok(format!("User {} deleted", user_id)),
        _ => Err(StatusCode::BAD_REQUEST),
    }
}

// Query parameter extraction
async fn search_users(Query(params): Query<Pagination>) -> Json<serde_json::Value> {
    let page = params.page.unwrap_or(1);
    let per_page = params.per_page.unwrap_or(10);
    
    Json(serde_json::json!({
        "page": page,
        "per_page": per_page,
        "results": [
            {"id": 1, "name": "User 1"},
            {"id": 2, "name": "User 2"}
        ]
    }))
}

// JSON body extraction
async fn create_user(Json(payload): Json<User>) -> Result<(StatusCode, Json<User>), StatusCode> {
    // Validation
    if payload.name.is_empty() {
        return Err(StatusCode::BAD_REQUEST);
    }
    
    // Create new user
    let user = User {
        id: 42, // ID retrieved from database
        name: payload.name,
        email: payload.email,
    };
    
    Ok((StatusCode::CREATED, Json(user)))
}

Middleware and Layers

use axum::{
    extract::Request,
    http::{header, HeaderValue, Method, StatusCode},
    middleware::{self, Next},
    response::Response,
    routing::get,
    Router,
};
use std::time::Duration;
use tower::{
    buffer::BufferLayer,
    limit::RateLimitLayer,
    ServiceBuilder,
};
use tower_http::{
    cors::{Any, CorsLayer},
    timeout::TimeoutLayer,
    trace::TraceLayer,
};

#[tokio::main]
async fn main() {
    // Tower middleware stack
    let middleware_stack = ServiceBuilder::new()
        .layer(TraceLayer::new_for_http())
        .layer(
            CorsLayer::new()
                .allow_origin(Any)
                .allow_methods([Method::GET, Method::POST])
                .allow_headers([header::CONTENT_TYPE]),
        )
        .layer(BufferLayer::new(1024))
        .layer(RateLimitLayer::new(5, Duration::from_secs(1)))
        .layer(TimeoutLayer::new(Duration::from_secs(30)));

    let app = Router::new()
        .route("/", get(|| async { "Hello" }))
        .route("/protected", get(protected_handler))
        .layer(middleware_stack)
        .layer(middleware::from_fn(custom_middleware))
        .route_layer(middleware::from_fn(auth_middleware));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

// Custom middleware
async fn custom_middleware(request: Request, next: Next) -> Response {
    println!("Processing request: {} {}", request.method(), request.uri());
    
    let response = next.run(request).await;
    
    println!("Response processed: {}", response.status());
    response
}

// Authentication middleware
async fn auth_middleware(mut request: Request, next: Next) -> Result<Response, StatusCode> {
    let auth_header = request
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|header| header.to_str().ok());

    match auth_header {
        Some(token) if token.starts_with("Bearer ") => {
            if token == "Bearer valid_token" {
                // Insert user info into request
                request.extensions_mut().insert(UserId(123));
                Ok(next.run(request).await)
            } else {
                Err(StatusCode::UNAUTHORIZED)
            }
        }
        _ => Err(StatusCode::UNAUTHORIZED),
    }
}

#[derive(Clone)]
struct UserId(u32);

async fn protected_handler() -> &'static str {
    "Protected resource"
}

State Management and Database Integration

use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::Json,
    routing::{get, post},
    Router,
};
use serde::{Deserialize, Serialize};
use sqlx::{PgPool, Row};
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    db: PgPool,
}

#[derive(Serialize, Deserialize)]
struct User {
    id: i32,
    name: String,
    email: String,
}

#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

#[tokio::main]
async fn main() {
    // Database connection pool
    let database_url = std::env::var("DATABASE_URL")
        .unwrap_or_else(|_| "postgres://user:password@localhost/axum_db".to_string());
    
    let pool = PgPool::connect(&database_url)
        .await
        .expect("Failed to connect to database");

    // Application state
    let app_state = AppState { db: pool };

    let app = Router::new()
        .route("/users", get(get_users).post(create_user))
        .route("/users/:id", get(get_user))
        .with_state(app_state);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

// Get all users
async fn get_users(State(state): State<AppState>) -> Result<Json<Vec<User>>, StatusCode> {
    let rows = sqlx::query("SELECT id, name, email FROM users")
        .fetch_all(&state.db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    let users: Vec<User> = rows
        .iter()
        .map(|row| User {
            id: row.get("id"),
            name: row.get("name"),
            email: row.get("email"),
        })
        .collect();

    Ok(Json(users))
}

// Get specific user
async fn get_user(
    Path(user_id): Path<i32>,
    State(state): State<AppState>,
) -> Result<Json<User>, StatusCode> {
    let row = sqlx::query("SELECT id, name, email FROM users WHERE id = $1")
        .bind(user_id)
        .fetch_optional(&state.db)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    match row {
        Some(row) => {
            let user = User {
                id: row.get("id"),
                name: row.get("name"),
                email: row.get("email"),
            };
            Ok(Json(user))
        }
        None => Err(StatusCode::NOT_FOUND),
    }
}

// Create user
async fn create_user(
    State(state): State<AppState>,
    Json(payload): Json<CreateUser>,
) -> Result<(StatusCode, Json<User>), StatusCode> {
    let row = sqlx::query(
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email"
    )
    .bind(&payload.name)
    .bind(&payload.email)
    .fetch_one(&state.db)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    let user = User {
        id: row.get("id"),
        name: row.get("name"),
        email: row.get("email"),
    };

    Ok((StatusCode::CREATED, Json(user)))
}

Error Handling and Custom Responses

use axum::{
    extract::rejection::JsonRejection,
    http::StatusCode,
    response::{IntoResponse, Response},
    routing::post,
    Json, Router,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("Validation error: {0}")]
    ValidationError(String),
    #[error("Database error")]
    DatabaseError(#[from] sqlx::Error),
    #[error("JSON parse error")]
    JsonExtractorError(#[from] JsonRejection),
    #[error("Authentication error")]
    AuthenticationError,
}

#[derive(Serialize)]
struct ErrorResponse {
    error: String,
    message: String,
    code: u16,
}

// Convert AppError to response
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status_code, message) = match self {
            AppError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg),
            AppError::DatabaseError(_) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Database error occurred".to_string(),
            ),
            AppError::JsonExtractorError(_) => (
                StatusCode::BAD_REQUEST,
                "Invalid JSON format".to_string(),
            ),
            AppError::AuthenticationError => (
                StatusCode::UNAUTHORIZED,
                "Authentication required".to_string(),
            ),
        };

        let error_response = ErrorResponse {
            error: self.to_string(),
            message,
            code: status_code.as_u16(),
        };

        (status_code, Json(error_response)).into_response()
    }
}

#[derive(Deserialize)]
struct UserInput {
    name: String,
    email: String,
    age: u32,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/users", post(create_user_with_validation))
        .route("/test-error", post(test_error_handler));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

// User creation with validation
async fn create_user_with_validation(
    Json(input): Json<UserInput>,
) -> Result<(StatusCode, Json<serde_json::Value>), AppError> {
    // Validation
    if input.name.trim().is_empty() {
        return Err(AppError::ValidationError("Name is required".to_string()));
    }
    
    if !input.email.contains('@') {
        return Err(AppError::ValidationError(
            "Please enter a valid email address".to_string(),
        ));
    }
    
    if input.age < 18 {
        return Err(AppError::ValidationError(
            "Must be 18 or older".to_string(),
        ));
    }

    // Success response
    Ok((
        StatusCode::CREATED,
        Json(serde_json::json!({
            "message": "User created successfully",
            "user": {
                "name": input.name,
                "email": input.email,
                "age": input.age
            }
        })),
    ))
}

// Error test handler
async fn test_error_handler() -> Result<&'static str, AppError> {
    // Intentionally cause an error
    Err(AppError::DatabaseError(sqlx::Error::RowNotFound))
}

WebSocket and File Serving

use axum::{
    extract::{ws::{Message, WebSocket, WebSocketUpgrade}, Path},
    http::StatusCode,
    response::{Html, IntoResponse},
    routing::{get, post},
    Router,
};
use futures_util::{sink::SinkExt, stream::StreamExt};
use tower_http::services::ServeDir;
use tokio::fs;

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index_handler))
        .route("/ws", get(websocket_handler))
        .route("/upload", post(upload_handler))
        .nest_service("/static", ServeDir::new("static"));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("Server running on: http://localhost:3000");
    axum::serve(listener, app).await.unwrap();
}

// Main page
async fn index_handler() -> Html<&'static str> {
    Html(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <title>Axum WebSocket Demo</title>
        <meta charset="utf-8">
    </head>
    <body>
        <h1>Axum WebSocket and File Serving Demo</h1>
        
        <h2>WebSocket Test</h2>
        <div>
            <input type="text" id="messageInput" placeholder="Enter message">
            <button onclick="sendMessage()">Send</button>
        </div>
        <div id="messages"></div>
        
        <h2>File Upload</h2>
        <form action="/upload" method="post" enctype="multipart/form-data">
            <input type="file" name="file" required>
            <button type="submit">Upload</button>
        </form>
        
        <script>
            const ws = new WebSocket('ws://localhost:3000/ws');
            const messages = document.getElementById('messages');
            
            ws.onmessage = function(event) {
                const div = document.createElement('div');
                div.textContent = 'Server: ' + event.data;
                messages.appendChild(div);
            };
            
            function sendMessage() {
                const input = document.getElementById('messageInput');
                ws.send(input.value);
                
                const div = document.createElement('div');
                div.textContent = 'Client: ' + input.value;
                messages.appendChild(div);
                
                input.value = '';
            }
        </script>
    </body>
    </html>
    "#)
}

// WebSocket handler
async fn websocket_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
    ws.on_upgrade(handle_socket)
}

async fn handle_socket(mut socket: WebSocket) {
    println!("WebSocket connection established");
    
    while let Some(msg) = socket.recv().await {
        match msg {
            Ok(Message::Text(text)) => {
                println!("Received message: {}", text);
                let response = format!("Echo: {}", text);
                
                if socket.send(Message::Text(response)).await.is_err() {
                    println!("Client disconnected");
                    break;
                }
            }
            Ok(Message::Close(_)) => {
                println!("WebSocket connection closed");
                break;
            }
            Err(e) => {
                println!("WebSocket error: {}", e);
                break;
            }
            _ => {}
        }
    }
}

// File upload handler
async fn upload_handler() -> impl IntoResponse {
    // In real implementation, would parse multipart data
    // Here returning a simple example with fixed response
    Html(r#"
    <h1>File Upload Complete</h1>
    <p>File was uploaded successfully.</p>
    <a href="/">Go back</a>
    "#)
}