axum

Applies to: Axum 0.7+, Rust Web APIs, Microservices Complements: .claude/skills/rust-guide/SKILL.md

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "axum" with this command: npx skills add ar4mirez/samuel/ar4mirez-samuel-axum

Axum Framework Guide

Applies to: Axum 0.7+, Rust Web APIs, Microservices Complements: .claude/skills/rust-guide/SKILL.md

Core Principles

  • Tower-First: Axum is built on Tower; embrace Service , Layer , and middleware composition

  • Type-Safe Extraction: Use compile-time extractors for request data -- no manual parsing

  • Async Throughout: All handlers are async; never block the Tokio runtime

  • Modular Routing: Compose routers with merge and nest ; separate public from protected routes

  • Structured Errors: Implement IntoResponse for custom error types; return appropriate HTTP status codes

Project Structure

myproject/ ├── Cargo.toml ├── src/ │ ├── main.rs # Entry point (thin: env, tracing, bind, serve) │ ├── lib.rs # Module declarations │ ├── config.rs # Configuration loading (env-based) │ ├── routes/ │ │ ├── mod.rs # Router composition (create_router) │ │ ├── users.rs # User-related route definitions │ │ └── health.rs # Health check route │ ├── handlers/ │ │ ├── mod.rs │ │ └── users.rs # Handler functions (thin: extract, call service, respond) │ ├── models/ │ │ ├── mod.rs │ │ └── user.rs # Domain types, DTOs, validation │ ├── services/ │ │ ├── mod.rs │ │ └── user_service.rs # Business logic layer │ ├── repositories/ │ │ ├── mod.rs │ │ └── user_repository.rs # Database access layer │ ├── extractors/ │ │ ├── mod.rs │ │ └── auth.rs # Custom extractors (AuthUser, Claims) │ ├── middleware/ │ │ ├── mod.rs │ │ └── logging.rs # Custom Tower middleware │ └── errors/ │ ├── mod.rs │ └── app_error.rs # AppError enum + IntoResponse ├── tests/ │ └── integration_tests.rs └── migrations/

Architectural rules:

  • Handlers are thin: extract request data, call service, return response

  • Services contain business logic; they call repositories

  • Repositories handle database access only

  • Models define domain types and DTOs with validation

  • Errors are defined per-layer and composed with #[from]

Dependencies (Cargo.toml)

[dependencies]

Web framework

axum = { version = "0.7", features = ["macros"] } axum-extra = { version = "0.9", features = ["typed-header"] } tokio = { version = "1.0", features = ["full"] } tower = { version = "0.4", features = ["full"] } tower-http = { version = "0.5", features = ["cors", "trace", "compression-gzip"] }

Serialization

serde = { version = "1.0", features = ["derive"] } serde_json = "1.0"

Database

sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "uuid", "chrono"] }

Validation

validator = { version = "0.16", features = ["derive"] }

Error handling

thiserror = "1.0" anyhow = "1.0"

Logging

tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] }

Configuration

config = "0.14" dotenvy = "0.15"

Utilities

uuid = { version = "1.0", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] }

[dev-dependencies] axum-test = "14.0" mockall = "0.12"

Application Entry Point

// src/main.rs #[tokio::main] async fn main() -> anyhow::Result<()> { dotenvy::dotenv().ok();

tracing_subscriber::registry()
    .with(tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| "myproject=debug,tower_http=debug".into()))
    .with(tracing_subscriber::fmt::layer())
    .init();

let config = config::Config::load()?;
let pool = sqlx::PgPool::connect(&#x26;config.database_url).await?;
sqlx::migrate!("./migrations").run(&#x26;pool).await?;

let state = AppState::new(pool, config.clone());
let app = routes::create_router(state);

let addr = SocketAddr::from(([0, 0, 0, 0], config.port));
tracing::info!("Starting server on {}", addr);

let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app)
    .with_graceful_shutdown(shutdown_signal())
    .await?;

Ok(())

}

async fn shutdown_signal() { tokio::signal::ctrl_c().await .expect("Failed to install CTRL+C signal handler"); tracing::info!("Shutdown signal received"); }

Routing

Router Composition

// src/routes/mod.rs pub fn create_router(state: AppState) -> Router { let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any);

let public_routes = Router::new()
    .route("/health", get(handlers::health::health_check))
    .route("/api/v1/auth/register", post(handlers::users::register))
    .route("/api/v1/auth/login", post(handlers::users::login));

let protected_routes = Router::new()
    .route("/api/v1/users", get(handlers::users::list_users))
    .route("/api/v1/users/:id", get(handlers::users::get_user))
    .route("/api/v1/users/:id", put(handlers::users::update_user))
    .route("/api/v1/users/:id", delete(handlers::users::delete_user))
    .layer(middleware::from_fn_with_state(
        state.clone(),
        app_middleware::auth::auth_middleware,
    ));

Router::new()
    .merge(public_routes)
    .merge(protected_routes)
    .layer(TraceLayer::new_for_http())
    .layer(CompressionLayer::new())
    .layer(cors)
    .with_state(state)

}

Routing Rules

  • Use merge for flat route composition; use nest for path-prefix grouping

  • Apply authentication middleware only to protected route groups

  • Layer order matters: outermost layer runs first (CORS, Tracing, then Compression)

  • Always call .with_state(state) last, after all routes and layers

Handlers

Handlers are async functions that receive extractors and return impl IntoResponse .

// src/handlers/users.rs pub async fn register( State(state): State<AppState>, Json(dto): Json<CreateUserDto>, ) -> AppResult<(StatusCode, Json<AuthResponse>)> { let service = UserService::new(state.pool, state.config); let response = service.register(dto).await?; Ok((StatusCode::CREATED, Json(response))) }

pub async fn list_users( State(state): State<AppState>, Query(pagination): Query<PaginationQuery>, _auth_user: AuthUser, ) -> AppResult<Json<Vec<UserResponse>>> { let service = UserService::new(state.pool, state.config); let users = service.list_users(pagination.page, pagination.per_page).await?; Ok(Json(users)) }

Handler Rules

  • Keep handlers under 15 lines; delegate logic to services

  • Always validate input via extractors or explicit .validate() calls

  • Use AppResult<T> (a type alias for Result<T, AppError> ) as the return type

  • Extract AuthUser even if unused (_auth_user ) to enforce authentication

  • Return appropriate status codes: CREATED for POST, NO_CONTENT for DELETE

Extractors

Built-in Extractors

Extractor Purpose Example

State(state)

Shared application state State(state): State<AppState>

Json(body)

JSON request body Json(dto): Json<CreateUserDto>

Path(id)

URL path parameters Path(id): Path<Uuid>

Query(params)

Query string parameters Query(q): Query<PaginationQuery>

Extension(ext)

Request extensions Extension(user): Extension<User>

Custom Extractor

// src/extractors/auth.rs pub struct AuthUser { pub user_id: Uuid, pub email: String, pub role: String, }

#[async_trait] impl FromRequestParts<AppState> for AuthUser { type Rejection = AppError;

async fn from_request_parts(
    parts: &#x26;mut Parts,
    state: &#x26;AppState,
) -> Result&#x3C;Self, Self::Rejection> {
    let TypedHeader(Authorization(bearer)) = parts
        .extract::&#x3C;TypedHeader&#x3C;Authorization&#x3C;Bearer>>>()
        .await
        .map_err(|_| AppError::Unauthorized("Missing authorization header".into()))?;

    let claims = verify_token(bearer.token(), &#x26;state.config.jwt_secret)?;

    Ok(AuthUser {
        user_id: claims.sub,
        email: claims.email,
        role: claims.role,
    })
}

}

Extractor Rules

  • Implement FromRequestParts (not FromRequest ) when you only need headers/state

  • Use FromRequest only when you need the body (consumes it)

  • Extractor order in function signatures matters: body-consuming extractors must be last

  • Always return your AppError type as the Rejection

State Management

#[derive(Clone)] pub struct AppState { pub pool: sqlx::PgPool, pub config: Config, }

impl AppState { pub fn new(pool: sqlx::PgPool, config: Config) -> Self { Self { pool, config } } }

State Rules

  • AppState must implement Clone (Axum requirement)

  • Use sqlx::PgPool directly (it is Arc -wrapped internally)

  • For mutable shared state, wrap in Arc<tokio::sync::RwLock<T>>

  • Avoid std::sync::Mutex in async state; use tokio::sync::Mutex or RwLock

  • Keep state lean: connection pools, config, caches -- not request-scoped data

Error Handling

// src/errors/app_error.rs #[derive(Error, Debug)] pub enum AppError { #[error("Not found: {0}")] NotFound(String),

#[error("Bad request: {0}")]
BadRequest(String),

#[error("Unauthorized: {0}")]
Unauthorized(String),

#[error("Forbidden: {0}")]
Forbidden(String),

#[error("Conflict: {0}")]
Conflict(String),

#[error("Validation error: {0}")]
Validation(String),

#[error("Internal server error")]
Internal(#[from] anyhow::Error),

#[error("Database error")]
Database(#[from] sqlx::Error),

}

impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, message) = match &self { AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()), AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()), AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg.clone()), AppError::Conflict(msg) => (StatusCode::CONFLICT, msg.clone()), AppError::Validation(msg) => (StatusCode::UNPROCESSABLE_ENTITY, msg.clone()), AppError::Internal(err) => { tracing::error!("Internal error: {:?}", err); (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".into()) } AppError::Database(err) => { tracing::error!("Database error: {:?}", err); (StatusCode::INTERNAL_SERVER_ERROR, "Database error".into()) } };

    let body = Json(json!({
        "success": false,
        "error": { "code": status.as_u16(), "message": message }
    }));

    (status, body).into_response()
}

}

pub type AppResult<T> = Result<T, AppError>;

Error Handling Rules

  • Always implement IntoResponse for your error type

  • Never leak internal details (database errors, stack traces) to clients

  • Log internal/database errors with tracing::error! before mapping to generic messages

  • Use thiserror for enum definitions; anyhow for internal propagation

  • Define AppResult<T> alias at the crate level for consistency

Middleware and Tower Integration

Route-Level Middleware

// Apply auth middleware to a route group let protected = Router::new() .route("/api/v1/users", get(list_users)) .layer(middleware::from_fn_with_state(state.clone(), auth_middleware));

Custom Middleware Function

pub async fn auth_middleware( State(state): State<AppState>, request: Request, next: Next, ) -> Result<Response, AppError> { let token = request.headers() .get(AUTHORIZATION) .and_then(|h| h.to_str().ok()) .and_then(|h| h.strip_prefix("Bearer ")) .ok_or_else(|| AppError::Unauthorized("Missing token".into()))?;

verify_token(token, &#x26;state.config.jwt_secret)?;
Ok(next.run(request).await)

}

Tower Layers (tower-http)

// Common layers -- add in create_router .layer(TraceLayer::new_for_http()) // Request/response logging .layer(CompressionLayer::new()) // Gzip response compression .layer(CorsLayer::new().allow_origin(Any)) // CORS headers .layer(TimeoutLayer::new(Duration::from_secs(30))) // Request timeout

Middleware Rules

  • Use middleware::from_fn_with_state for middleware needing AppState

  • Use middleware::from_fn for stateless middleware

  • Layer ordering: outermost layer executes first on request, last on response

  • Always add TimeoutLayer to prevent slow requests from exhausting resources

  • Use TraceLayer for observability in all environments

Commands

Development

cargo run # Start server cargo watch -x run # Watch mode (requires cargo-watch) RUST_LOG=debug cargo run # With debug logging

Build

cargo build --release # Production build cargo check # Fast type-check

Quality

cargo fmt # Format code cargo clippy -- -D warnings # Lint with deny cargo audit # Vulnerability check

Testing

cargo test # All tests cargo test --test integration_tests # Integration only

Database

sqlx migrate run # Run migrations sqlx migrate add create_users # Create new migration

Best Practices Summary

  • Extractors: Use custom extractors for reusable validation; FromRequestParts for headers, FromRequest for body

  • Error Handling: One AppError enum per service; implement IntoResponse ; log internal errors, sanitize client messages

  • Performance: Use connection pooling (SQLx), enable compression (tower-http), add timeouts, use async throughout

  • Testing: Use axum-test for integration tests; mock repositories for unit tests; use a separate test database

  • Security: Validate all inputs with validator ; use parameterized queries (SQLx); never expose internal errors

Advanced Topics

For detailed handler examples, database integration, authentication flows, WebSocket support, and testing patterns, see:

  • references/patterns.md -- Full CRUD handlers, repository pattern, JWT auth, WebSocket, integration testing

External References

  • Axum Documentation

  • Tower Documentation

  • Tokio Documentation

  • SQLx Documentation

  • Axum Examples

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

actix-web

No summary provided by upstream source.

Repository SourceNeeds Review
General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

blazor

No summary provided by upstream source.

Repository SourceNeeds Review
General

fiber

No summary provided by upstream source.

Repository SourceNeeds Review