xiaohongshu
xiaohongshu
axum-0-8-expert
Expert guidance for Axum 0.8.x web framework development in Rust. Use when working with Axum 0.8+, migrating from 0.7 to 0.8, or when users mention path parameter syntax issues, async_trait problems, or Option extractor changes.
Full skill instructions
This skill provides expert guidance for developing with Axum 0.8.x, the ergonomic and modular web framework built with Tokio, Tower, and Hyper. Axum 0.8 was released in January 2025 and includes several breaking changes from 0.7.
Old Syntax (0.7):
Router::new()
.route("/users/:id", get(handler))
.route("/files/*path", get(catch_all))
New Syntax (0.8):
Router::new()
.route("/users/{id}", get(handler))
.route("/files/{*path}", get(catch_all))
Migration:
:param with {param}*param with {*param}Examples:
// Single parameter
.route("/users/{user_id}", get(get_user))
// Multiple parameters
.route("/users/{user_id}/posts/{post_id}", get(get_post))
// Catch-all parameter
.route("/files/{*path}", get(serve_file))
// Extracting in handlers
async fn get_user(Path(user_id): Path<String>) { }
async fn get_post(Path((user_id, post_id)): Path<(String, String)>) { }
async_trait Macro Removal (BREAKING)Rust now has native support for async trait methods (RPITIT - Return Position Impl Trait In Traits), so the #[async_trait] macro is no longer needed.
Migration:
// OLD (0.7)
use axum::async_trait;
#[async_trait]
impl<S> FromRequestParts<S> for MyExtractor
where
S: Send + Sync,
{
type Rejection = MyRejection;
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
// ...
}
}
// NEW (0.8)
use async_trait::async_trait; // Use this if you still need it elsewhere
impl<S> FromRequestParts<S> for MyExtractor
where
S: Send + Sync,
{
type Rejection = MyRejection;
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
// ...
}
}
Important:
#[async_trait] from custom FromRequestParts and FromRequest implementationsasync_trait for other traits, import it from the async-trait crate directlyasync-trait = "0.1" to your Cargo.toml if neededOption<T> Extractor Behavior Change (BREAKING)Previously, Option<T> would silently swallow ANY rejection and return None. Now it requires T to implement OptionalFromRequestParts or OptionalFromRequest.
Old Behavior (0.7):
// This would ALWAYS succeed, even if token was invalid
async fn handler(user: Option<AuthenticatedUser>) {
match user {
Some(user) => // authenticated
None => // could be missing OR invalid token
}
}
New Behavior (0.8):
// This can now fail if the token is invalid
async fn handler(user: Option<AuthenticatedUser>) -> Result<Response, StatusCode> {
match user {
Some(user) => // authenticated with valid token
None => // missing token (but would return error for invalid token)
}
}
Migration Strategy:
For extractors that should be truly optional (missing = None, invalid = error):
use axum::extract::rejection::OptionalFromRequestPartsError;
impl OptionalFromRequestParts<S> for AuthenticatedUser
where
S: Send + Sync,
{
type Rejection = OptionalFromRequestPartsError<MyRejection>;
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> Result<Option<Self>, Self::Rejection> {
match Self::from_request_parts(parts, state).await {
Ok(user) => Ok(Some(user)),
Err(rejection) if rejection.is_missing() => Ok(None),
Err(rejection) => Err(OptionalFromRequestPartsError::Inner(rejection)),
}
}
}
For truly optional behavior (ignore all rejections):
// Use Result instead
async fn handler(user: Result<AuthenticatedUser, AuthRejection>) {
match user {
Ok(user) => // authenticated
Err(_) => // missing or invalid
}
}
Multiple Extractors (Order Matters):
async fn handler(
Path(id): Path<String>, // Path first
State(state): State<AppState>, // State second
Query(params): Query<SearchParams>, // Query params
Json(body): Json<CreateRequest>, // Body last
) -> impl IntoResponse {
// ...
}
Optional Parameters:
async fn handler(
Path(id): Path<String>,
pagination: Option<Query<Pagination>>,
) -> impl IntoResponse {
let Query(pagination) = pagination.unwrap_or_default();
// ...
}
Struct-based (Recommended for multiple params):
#[derive(Deserialize)]
struct UserParams {
user_id: Uuid,
team_id: Uuid,
}
async fn handler(Path(UserParams { user_id, team_id }): Path<UserParams>) {
// ...
}
Router::new().route("/users/{user_id}/teams/{team_id}", get(handler))
Tuple-based (Quick for 2-3 params):
async fn handler(Path((user_id, team_id)): Path<(Uuid, Uuid)>) {
// ...
}
HashMap/Vec for dynamic parameters:
use std::collections::HashMap;
async fn handler(Path(params): Path<HashMap<String, String>>) {
// All path parameters as key-value pairs
}
#[derive(Clone)]
struct AppState {
db: PgPool,
redis: RedisClient,
}
let state = AppState { db, redis };
let app = Router::new()
.route("/", get(handler))
.with_state(state);
async fn handler(State(state): State<AppState>) -> impl IntoResponse {
// Access state.db, state.redis
}
Custom Rejection Types:
use axum::{
response::{IntoResponse, Response},
http::StatusCode,
};
struct MyError(anyhow::Error);
impl IntoResponse for MyError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Error: {}", self.0),
).into_response()
}
}
impl<E> From<E> for MyError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}
async fn handler() -> Result<Json<Response>, MyError> {
let data = fetch_data().await?;
Ok(Json(data))
}
Cargo.toml:[dependencies]
axum = "0.8"
tokio = { version = "1.0", features = ["full"] }
# Add if you use async_trait elsewhere:
async-trait = "0.1"
Update all route paths:
/: → /{ and add closing }/* → /{*Remove #[async_trait] from extractors:
impl FromRequestParts and impl FromRequest#[async_trait] attributeuse axum::async_trait; to use async_trait::async_trait; if needed elsewhereReview Option<T> extractors:
Option<CustomExtractor>OptionalFromRequestParts if neededTest all routes:
// In 0.8, fallback behavior with nested routers may differ
// Test your fallback routes carefully after migration
let api = Router::new()
.route("/users", get(users))
.fallback(api_fallback);
let app = Router::new()
.nest("/api", api)
.fallback(app_fallback);
// get_service is removed in favor of more specific methods
// OLD: .route("/assets/*path", get_service(ServeDir::new("assets")))
// NEW: Use fallback_service or route_service
.route_service("/assets/*path", ServeDir::new("assets"))
Axum 0.8 works with http-body 1.0. Ensure your body types are compatible.
use validator::Validate;
#[derive(Deserialize, Validate)]
struct SearchQuery {
#[validate(length(min = 1, max = 100))]
q: String,
#[validate(range(min = 1, max = 100))]
limit: Option<u32>,
}
async fn search(Query(query): Query<SearchQuery>) -> Result<Json<Results>, StatusCode> {
query.validate().map_err(|_| StatusCode::BAD_REQUEST)?;
// ...
}
[dependencies]
axum = "0.8"
tokio = { version = "1.0", features = ["full"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["trace", "cors"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Database
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres"] }
# Validation
validator = { version = "0.18", features = ["derive"] }
# UUID & Time
uuid = { version = "1.0", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
# Tracing
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
Use State efficiently:
Arc<T> wrapped stateArc<AppState> directly in your stateAvoid unnecessary cloning:
// Good: Use references where possible
async fn handler(State(state): State<Arc<AppState>>) {
// state is Arc, cloning is cheap
}
Use middleware wisely:
use tower_http::trace::TraceLayer;
let app = Router::new()
.route("/", get(handler))
.layer(TraceLayer::new_for_http());
#[cfg(test)]
mod tests {
use super::*;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
#[tokio::test]
async fn test_route() {
let app = app();
let response = app
.oneshot(
Request::builder()
.uri("/users/123")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience