
Axum
- 36 installs
- 27 repo stars
- Updated July 17, 2026
- claude-dev-suite/claude-dev-suite
Build async Rust web APIs with Axum using routing, handlers, extractors, tower middleware, and shared state.
About
Reference for the Axum Rust web framework covering routing, handlers, extractors, tower middleware, and state. A developer uses it when building ergonomic async Rust APIs on Tokio.
- Router, handlers, and extractor patterns
- Tower middleware and shared state
Axum by the numbers
- 36 all-time installs (skills.sh)
- Ranked #79 of 121 Rust skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill axumAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 36 |
|---|---|
| repo stars | ★ 27 |
| Last updated | July 17, 2026 |
| Repository | claude-dev-suite/claude-dev-suite ↗ |
What it does
Build async Rust web APIs with Axum using routing, handlers, extractors, tower middleware, and shared state.
Files
Axum Core Knowledge
Full Reference: See advanced.md for authentication middleware, WebSocket handling, graceful shutdown, and custom error types.
Deep Knowledge: Usemcp__documentation__fetch_docswith technology:axumfor comprehensive documentation.
Basic Setup
# Cargo.toml
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
tower-http = { version = "0.5", features = ["cors", "trace"] }use axum::{routing::get, Router};
async fn hello() -> &'static str {
"Hello, World!"
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(hello));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
axum::serve(listener, app).await.unwrap();
}Routing
let app = Router::new()
.route("/", get(index))
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user).put(update_user).delete(delete_user));
// Nested Routes
let api_routes = Router::new()
.route("/users", get(list_users));
let app = Router::new().nest("/api/v1", api_routes);Extractors
use axum::extract::{Path, Query, Json, State};
// Path parameters
async fn get_user(Path(id): Path<u32>) -> String {
format!("User {}", id)
}
// Query parameters
#[derive(Deserialize)]
struct Pagination { page: Option<u32>, per_page: Option<u32> }
async fn list_users(Query(pagination): Query<Pagination>) -> Json<Value> {
Json(json!({ "page": pagination.page.unwrap_or(1) }))
}
// JSON body
async fn create_user(Json(payload): Json<CreateUser>) -> (StatusCode, Json<Value>) {
(StatusCode::CREATED, Json(json!({ "name": payload.name })))
}Application State
use std::sync::Arc;
struct AppState {
db_pool: sqlx::PgPool,
}
async fn handler(State(state): State<Arc<AppState>>) -> String {
// Use state.db_pool
}
let state = Arc::new(AppState { db_pool: pool });
let app = Router::new()
.route("/", get(handler))
.with_state(state);Tower Middleware
use tower_http::{cors::CorsLayer, trace::TraceLayer};
use tower::ServiceBuilder;
let app = Router::new()
.route("/", get(index))
.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive())
);Health Checks
async fn health() -> Json<Value> {
Json(json!({ "status": "healthy" }))
}
async fn ready(State(state): State<Arc<AppState>>) -> Result<Json<Value>, StatusCode> {
sqlx::query("SELECT 1")
.execute(&state.db_pool)
.await
.map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
Ok(Json(json!({ "status": "ready" })))
}When NOT to Use This Skill
- Actix-web projects - Actix has more built-in features
- Rocket projects - Rocket has compile-time route checking
- Sync-only Rust code - Axum requires async runtime
Anti-Patterns
| Anti-Pattern | Why It's Bad | Solution |
|---|---|---|
Not using Arc for state | Expensive clones | Wrap state in Arc<AppState> |
| Blocking operations in handlers | Blocks executor | Use tokio::task::spawn_blocking |
| Missing error conversion | Compiler errors | Implement IntoResponse for errors |
| Not using extractors | Manual parsing | Use Path, Query, Json extractors |
Quick Troubleshooting
| Problem | Diagnosis | Fix |
|---|---|---|
| "Handler doesn't implement Handler" | Wrong signature | Check extractor order and return type |
| Route not matching | Conflicting routes | Order routes from specific to general |
| State not accessible | Wrong state type | Ensure with_state() matches State<T> |
| Missing CORS headers | No layer | Add CorsLayer from tower-http |
Production Checklist
- [ ] Tracing/logging configured
- [ ] CORS properly set up
- [ ] Error handling with custom types
- [ ] Health/readiness endpoints
- [ ] Graceful shutdown
- [ ] State management with Arc
- [ ] Input validation
Reference Documentation
- Extractors
- Middleware
Axum - Advanced Patterns
Authentication Middleware
use axum::{
middleware::{self, Next},
extract::{Request, State},
response::Response,
http::StatusCode,
};
#[derive(Clone)]
struct CurrentUser {
id: u32,
name: String,
}
async fn auth_middleware(
State(state): State<Arc<AppState>>,
mut request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let auth_header = request
.headers()
.get("authorization")
.and_then(|h| h.to_str().ok());
match auth_header {
Some(token) if token.starts_with("Bearer ") => {
let token = &token[7..];
match verify_token(token, &state).await {
Ok(user) => {
request.extensions_mut().insert(user);
Ok(next.run(request).await)
}
Err(_) => Err(StatusCode::UNAUTHORIZED),
}
}
_ => Err(StatusCode::UNAUTHORIZED),
}
}
// Extract user in handler
async fn protected_handler(
Extension(user): Extension<CurrentUser>,
) -> String {
format!("Hello, {}", user.name)
}
let protected_routes = Router::new()
.route("/me", get(protected_handler))
.layer(middleware::from_fn_with_state(state.clone(), auth_middleware));WebSocket
use axum::{
extract::ws::{Message, WebSocket, WebSocketUpgrade},
response::Response,
};
async fn ws_handler(ws: WebSocketUpgrade) -> Response {
ws.on_upgrade(handle_socket)
}
async fn handle_socket(mut socket: WebSocket) {
while let Some(msg) = socket.recv().await {
match msg {
Ok(Message::Text(text)) => {
if socket.send(Message::Text(format!("Echo: {}", text))).await.is_err() {
break;
}
}
Ok(Message::Close(_)) => break,
_ => {}
}
}
}
let app = Router::new().route("/ws", get(ws_handler));Graceful Shutdown
use tokio::signal;
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(index));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
tracing::info!("Shutdown signal received");
}Custom Timing Middleware
use axum::{
middleware::{self, Next},
extract::Request,
response::Response,
};
use std::time::Instant;
async fn timing_middleware(request: Request, next: Next) -> Response {
let start = Instant::now();
let response = next.run(request).await;
let duration = start.elapsed();
tracing::info!("Request completed in {:?}", duration);
response
}
let app = Router::new()
.route("/", get(index))
.layer(middleware::from_fn(timing_middleware));Error Handling
use axum::{response::{IntoResponse, Response}, http::StatusCode};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Not found: {0}")]
NotFound(String),
#[error("Bad request: {0}")]
BadRequest(String),
#[error("Internal error")]
Internal(#[from] 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(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error".into()),
};
(status, Json(serde_json::json!({ "error": message }))).into_response()
}
}
// Usage
async fn get_user(Path(id): Path<u32>) -> Result<Json<User>, AppError> {
let user = find_user(id).await.ok_or_else(|| AppError::NotFound(format!("User {}", id)))?;
Ok(Json(user))
}Typed Header Extraction
use axum_extra::typed_header::TypedHeader;
use axum_extra::headers::Authorization;
use axum_extra::headers::authorization::Bearer;
async fn protected(TypedHeader(auth): TypedHeader<Authorization<Bearer>>) -> String {
format!("Token: {}", auth.token())
}Related skills
Rustbackend