Axum
Modern Rust web framework fully integrated with Tokio ecosystem. Excellent ergonomics with performance comparable to Actix-web.
GitHub Overview
tokio-rs/axum
Ergonomic and modular web framework built with Tokio, Tower, and Hyper
Topics
Star History
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
- Axum Official Documentation
- Axum GitHub Repository
- Axum Examples
- Tokio Official Site
- Tower GitHub Repository
- Axum Rust By Example Guide
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>
"#)
}