internal/skills/content/axum/SKILL.md
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.
npx skillsauth add ar4mirez/samuel axumInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Applies to: Axum 0.7+, Rust Web APIs, Microservices Complements:
.claude/skills/rust-guide/SKILL.md
Service, Layer, and middleware compositionmerge and nest; separate public from protected routesIntoResponse for custom error types; return appropriate HTTP status codesmyproject/
├── 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:
#[from][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"
// 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(&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");
}
// 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)
}
merge for flat route composition; use nest for path-prefix grouping.with_state(state) last, after all routes and layersHandlers 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))
}
.validate() callsAppResult<T> (a type alias for Result<T, AppError>) as the return typeAuthUser even if unused (_auth_user) to enforce authenticationCREATED for POST, NO_CONTENT for DELETE| 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> |
// 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: &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,
})
}
}
FromRequestParts (not FromRequest) when you only need headers/stateFromRequest only when you need the body (consumes it)AppError type as the Rejection#[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 }
}
}
AppState must implement Clone (Axum requirement)sqlx::PgPool directly (it is Arc-wrapped internally)Arc<tokio::sync::RwLock<T>>std::sync::Mutex in async state; use tokio::sync::Mutex or RwLock// 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>;
IntoResponse for your error typetracing::error! before mapping to generic messagesthiserror for enum definitions; anyhow for internal propagationAppResult<T> alias at the crate level for consistency// 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));
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)
}
// 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::from_fn_with_state for middleware needing AppStatemiddleware::from_fn for stateless middlewareTimeoutLayer to prevent slow requests from exhausting resourcesTraceLayer for observability in all environments# 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
FromRequestParts for headers, FromRequest for bodyAppError enum per service; implement IntoResponse; log internal errors, sanitize client messagesaxum-test for integration tests; mock repositories for unit tests; use a separate test databasevalidator; use parameterized queries (SQLx); never expose internal errorsFor detailed handler examples, database integration, authentication flows, WebSocket support, and testing patterns, see:
development
Zig language guardrails, patterns, and best practices for AI-assisted development. Use when working with Zig files (.zig), build.zig, or when the user mentions Zig. Provides comptime patterns, allocator conventions, C interop guidelines, and testing standards specific to this project's coding standards.
tools
WordPress framework guardrails, patterns, and best practices for AI-assisted development. Use when working with WordPress projects, or when the user mentions WordPress. Provides theme development, plugin architecture, REST API, blocks, and security guidelines.
tools
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs. Use when testing web apps, automating browser interactions, or debugging frontend issues.
tools
Suite of tools for creating elaborate, multi-component web applications using modern frontend technologies (React, Tailwind CSS, shadcn/ui). Use for complex projects requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX pages.