Axum is a modern Rust web framework built on top of Tower and Hyper, developed by the Tokio team. It features a type-safe, ergonomic API that leverages Rust's type system for compile-time correctness. If you prefer a more modular, middleware-composable approach compared to Actix-Web, Axum is an excellent choice for production web services.
Project Setup
# Create new project
cargo new axum-api && cd axum-api
# Cargo.toml dependencies
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace", "compression-gzip"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "macros"] }
dotenvy = "0.15"
thiserror = "1"
uuid = { version = "1", features = ["v4", "serde"] }
Application Code
// src/main.rs
use axum::{
extract::{Path, State, Json},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPoolOptions;
use std::sync::Arc;
use tower_http::{cors::CorsLayer, trace::TraceLayer, compression::CompressionLayer};
#[derive(Clone)]
struct AppState {
db: sqlx::PgPool,
}
#[derive(Serialize)]
struct User {
id: uuid::Uuid,
name: String,
email: String,
}
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
// Custom error type
#[derive(thiserror::Error, Debug)]
enum AppError {
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("Not found")]
NotFound,
}
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let (status, message) = match self {
AppError::Database(e) => {
tracing::error!("Database error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string())
}
AppError::NotFound => (StatusCode::NOT_FOUND, "Resource not found".to_string()),
};
(status, Json(serde_json::json!({"error": message}))).into_response()
}
}
async fn health() -> impl IntoResponse {
Json(serde_json::json!({"status": "healthy"}))
}
async fn list_users(State(state): State) -> Result {
let users = sqlx::query_as!(User, "SELECT id, name, email FROM users ORDER BY name")
.fetch_all(&state.db)
.await?;
Ok(Json(users))
}
async fn get_user(
State(state): State,
Path(id): Path,
) -> Result {
let user = sqlx::query_as!(User, "SELECT id, name, email FROM users WHERE id = $1", id)
.fetch_optional(&state.db)
.await?
.ok_or(AppError::NotFound)?;
Ok(Json(user))
}
async fn create_user(
State(state): State,
Json(input): Json,
) -> Result {
let user = sqlx::query_as!(
User,
"INSERT INTO users (id, name, email) VALUES ($1, $2, $3) RETURNING id, name, email",
uuid::Uuid::new_v4(), input.name, input.email
)
.fetch_one(&state.db)
.await?;
Ok((StatusCode::CREATED, Json(user)))
}
#[tokio::main]
async fn main() {
dotenvy::dotenv().ok();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "axum_api=info,tower_http=info".into()),
)
.init();
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL required");
let pool = PgPoolOptions::new()
.max_connections(20)
.connect(&database_url)
.await
.expect("Failed to connect to database");
let state = AppState { db: pool };
let app = Router::new()
.route("/health", get(health))
.route("/api/users", get(list_users).post(create_user))
.route("/api/users/:id", get(get_user))
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new())
.layer(CorsLayer::permissive())
.with_state(state);
let addr = std::env::var("BIND_ADDR").unwrap_or_else(|_| "0.0.0.0:8080".into());
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
tracing::info!("Listening on {}", addr);
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
}
async fn shutdown_signal() {
tokio::signal::ctrl_c().await.expect("Install Ctrl+C handler");
tracing::info!("Shutting down gracefully...");
}
Middleware and Extractors
// Custom middleware for request timing
use axum::middleware::{self, Next};
use axum::http::Request;
use std::time::Instant;
async fn timing_middleware(
request: Request,
next: Next,
) -> impl IntoResponse {
let start = Instant::now();
let method = request.method().clone();
let uri = request.uri().clone();
let response = next.run(request).await;
let duration = start.elapsed();
tracing::info!("{} {} - {:?} - {}", method, uri, duration, response.status());
response
}
// Add to router
let app = Router::new()
.route("/api/users", get(list_users))
.layer(middleware::from_fn(timing_middleware));
Deployment
# Build optimized release binary
cargo build --release
# Cargo.toml profile
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
strip = true
# Deploy binary + .env
scp target/release/axum-api user@vps:/opt/myapp/
scp .env user@vps:/opt/myapp/
# Systemd service (same pattern as Actix-Web)
# /etc/systemd/system/axum-api.service
[Unit]
Description=Axum API
After=network.target postgresql.service
[Service]
Type=simple
User=appuser
ExecStart=/opt/myapp/axum-api
EnvironmentFile=/opt/myapp/.env
Restart=always
RestartSec=3
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
Docker Deployment
# Multi-stage Dockerfile
FROM rust:1.79-alpine AS builder
RUN apk add musl-dev
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release --target x86_64-unknown-linux-musl
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/axum-api /
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER 65534
EXPOSE 8080
ENTRYPOINT ["/axum-api"]
# Build and run
docker build -t axum-api .
docker run -p 8080:8080 --env-file .env axum-api
Testing
// src/main.rs — add test module
#[cfg(test)]
mod tests {
use super::*;
use axum::http::StatusCode;
use axum_test::TestServer;
#[tokio::test]
async fn test_health() {
let app = Router::new().route("/health", get(health));
let server = TestServer::new(app).unwrap();
let response = server.get("/health").await;
response.assert_status(StatusCode::OK);
response.assert_json(&serde_json::json!({"status": "healthy"}));
}
}
Axum vs Actix-Web
| Feature | Axum | Actix-Web |
|---|---|---|
| Runtime | Tokio (official) | Actix runtime (Tokio-based) |
| Middleware | Tower ecosystem | Own middleware system |
| Type Safety | Compile-time extractors | Runtime extraction |
| Performance | Excellent | Slightly faster in raw benchmarks |
| Ecosystem | Tower/Hyper compatible | Actix-specific |
| Learning Curve | Moderate | Moderate |
Summary
Axum provides an elegant, type-safe web framework that integrates seamlessly with the Tokio ecosystem. Its use of Tower middleware means you can compose functionality from a rich ecosystem of existing middleware. Deployment is identical to any Rust application — compile a single binary, deploy with systemd, and reverse proxy with Nginx. For teams already using Tower or Hyper, Axum is the natural choice for building production web services.