axum-guide
Installation
SKILL.md
Axum v0.8.x Development Guide
Handlers
use axum::{extract::{Path, Query, State, Json}, response::IntoResponse, http::StatusCode};
async fn get_user(
State(pool): State<PgPool>,
Path(id): Path<i32>,
) -> Result<Json<User>, AppError> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_one(&pool).await?;
Ok(Json(user))
}
async fn create_user(
State(pool): State<PgPool>,
claims: Claims, // Custom extractor
Json(payload): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<User>), AppError> {
// ...
Ok((StatusCode::CREATED, Json(user)))
}
Routing
fn app() -> Router<AppState> {
Router::new()
.route("/", get(root))
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user).put(update_user).delete(delete_user))
.nest("/api", api_routes())
.layer(middleware_stack())
.with_state(state)
}
Error Handling
pub struct AppError {
pub code: StatusCode,
pub message: String,
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(self.code, Json(json!({"error": self.message}))).into_response()
}
}
impl From<sqlx::Error> for AppError {
fn from(err: sqlx::Error) -> Self {
match err {
sqlx::Error::RowNotFound => AppError {
code: StatusCode::NOT_FOUND,
message: "Resource not found".into(),
},
_ => AppError {
code: StatusCode::INTERNAL_SERVER_ERROR,
message: "Database error".into(),
},
}
}
}
Middleware
async fn logging_middleware(req: Request, next: Next) -> Response {
let method = req.method().clone();
let uri = req.uri().clone();
let response = next.run(req).await;
tracing::info!("{} {} -> {}", method, uri, response.status());
response
}
let app = Router::new()
.route("/", get(handler))
.layer(middleware::from_fn(logging_middleware));
Custom Extractor
pub struct Claims { pub user_id: i32, pub role: String }
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for Claims {
type Rejection = AppError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let token = parts.headers.get("cookie")
.and_then(|v| v.to_str().ok())
.and_then(extract_token)
.ok_or(AppError::unauthorized("No token"))?;
validate_jwt(&token)
}
}
Auth Middleware
async fn auth_middleware(claims: Claims, req: Request, next: Next) -> Response {
next.run(req).await
}
let protected = Router::new()
.route("/profile", get(profile))
.layer(middleware::from_fn(auth_middleware));
CORS
use tower_http::cors::{CorsLayer, Any};
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any)
.allow_credentials(true);
Response Headers & Cookies
async fn login() -> impl IntoResponse {
let cookie = format!(
"token={}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age={}",
token, max_age
);
([(header::SET_COOKIE, cookie)], Json(response))
}
File Upload
async fn upload(mut multipart: Multipart) -> Result<(), AppError> {
while let Some(field) = multipart.next_field().await? {
let name = field.name().unwrap_or_default().to_string();
let data = field.bytes().await?;
// Process file
}
Ok(())
}
Notes
- Extractor order: Put
Statelast (others may consume body) - Consistent error type: Use single AppError across app
- Middleware order: Executes outer→inner (reverse of layer order)
- State type: Must implement
Clone(usually wrap inArc)
Related skills
More from daiki48/dotfiles
sqlx-postgres
SQLx + PostgreSQL v17 database guide. Queries, migrations, ENUMs, JSONB, transactions.
63leptos-guide
Leptos v0.8.x frontend development guide. Components, signals, resources, async, forms, and ownership patterns.
57dioxus-guide
Dioxus v0.7.x desktop app guide. Components, Signal state, rsx! macro, hooks, events, Context API, async.
22rust-fullstack
Rust full-stack patterns. Leptos + Axum + PostgreSQL web apps, auth, multi-tenant, API design.
22rusqlite-guide
rusqlite + SQLite database guide. Connection, CRUD, transactions, FTS5 full-text search, migrations.
19fact-check
Claude内マルチモデルファクトチェック。Opusが自身でチェックし、Sonnetサブエージェントに独立チェックさせ、両者の結果を比較・議論して合意レポートを出力する。
12