Skip to main content
axum Axum framework guardrails, patterns, and best practices for AI-assisted development.
Use when working with Axum projects, or when the user mentions Axum framework.
Provides Tower integration, async handlers, extractors, and type-safe routing guidelines.
Ir para a instalação Skills Marketplace Descubra e explore skills de IA criadas pela comunidade.
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Copiar promptMostrar detalhes do prompt Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
npx skills add https://github.com/ar4mirez/samuel --skill axumO comando permanece em uma só linha. Role horizontalmente para revisá-lo antes de copiar.
Prefere uma cópia local? Baixe os arquivos disponíveis atualmente no SkillsMP.
Baixar Zip Baixando... Ocupações relacionadas
SOC
Baseado na classificação ocupacional SOC
Explorador de arquivos
2 arquivos name axum description Axum framework guardrails, patterns, and best practices for AI-assisted development.
Use when working with Axum projects, or when the user mentions Axum framework.
Provides Tower integration, async handlers, extractors, and type-safe routing guidelines.
license MIT metadata {"author":"samuel","version":"1.0","category":"framework","language":"rust","extensions":".rs"}
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/
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]
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" ] }
serde = { version = "1.0" , features = ["derive" ] }
serde_json = "1.0"
sqlx = { version = "0.7" , features = ["runtime-tokio" , "postgres" , "uuid" , "chrono" ] }
validator = { version = "0.16" , features = ["derive" ] }
thiserror = "1.0"
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3" , features = ["env-filter" ] }
config = "0.14"
dotenvy = "0.15"
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
#[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 (&config.database_url).await ?;
sqlx::migrate!("./migrations" ).run (&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
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.
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
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: &mut Parts,
state: &AppState,
) -> Result <Self , Self ::Rejection> {
let TypedHeader (Authorization (bearer)) = parts
.extract::<TypedHeader<Authorization<Bearer>>>()
.await
.map_err (|_| AppError::Unauthorized ("Missing authorization header" .into ()))?;
let claims = verify_token (bearer.token (), &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
#[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
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, &state.config.jwt_secret)?;
Ok (next.run (request).await )
}
Tower Layers (tower-http)
.layer (TraceLayer::new_for_http ())
.layer (CompressionLayer::new ())
.layer (CorsLayer::new ().allow_origin (Any))
.layer (TimeoutLayer::new (Duration::from_secs (30 )))
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
cargo run
cargo watch -x run
RUST_LOG=debug cargo run
cargo build --release
cargo check
cargo fmt
cargo clippy -- -D warnings
cargo audit
cargo test
cargo test --test integration_tests
sqlx migrate run
sqlx migrate add create_users
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:
External References