$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Rule Writing

CLAUDE.md for Axum (Rust) Web Framework

Axum is Tokio-native and tower-based. AI generates Actix patterns that don't apply. Rules for extractors, State, tower middleware, Router composition, and IntoResponse.

7 min read·February 14, 2026

Axum is tower-based, not Actix — AI confuses the two constantly

Extractors, State<T>, Router composition, IntoResponse, and tower middleware

Why Axum Needs Rules Distinct from Actix

Axum is Rust's Tokio-native web framework — built on tower (a middleware abstraction) and hyper (an HTTP implementation). Its API is fundamentally different from Actix Web: extractors are based on trait implementations (not macros), state is shared via State<T> (not web::Data), and middleware uses tower's Service/Layer system (not Actix's Transform). AI conflates the two — generating Actix patterns that don't compile in Axum.

Axum's design philosophy is 'minimal magic' — no macros for routing, no special derive macros for extractors, no framework-specific async runtime. Everything composes through standard Rust traits: IntoResponse, FromRequest, FromRequestParts. This makes Axum extremely flexible but also means AI needs to understand the trait-based API.

These rules target Axum 0.7+ with Tokio. They layer on top of Rust language rules and cover Axum-specific patterns for routing, extraction, state management, middleware, and error handling.

Rule 1: Extractors and Handler Signatures

The rule: 'Axum handlers are async functions whose parameters are extractors. Each parameter type must implement FromRequest or FromRequestParts. Common extractors: State<T> for shared state, Path<T> for path params, Query<T> for query params, Json<T> for JSON body. Extractors that consume the body (Json, String, Bytes) must be the last parameter. Order matters — body extractors after non-body extractors.'

For custom extractors: 'Implement FromRequestParts for extractors that don't need the body (auth tokens, custom headers): async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection>. Implement FromRequest for extractors that consume the body. Return a rejection type that implements IntoResponse for error cases.'

AI generates handler signatures with wrong extractor ordering (body before path — doesn't compile) or uses Actix's web::Path syntax. Axum extractors are plain types in function parameters — the framework dispatches based on the type's trait implementation.

  • Parameters are extractors: State<T>, Path<T>, Query<T>, Json<T>
  • Body extractors (Json, String, Bytes) must be the LAST parameter
  • Non-body extractors (State, Path, Query, headers) in any order before body
  • Custom extractors: FromRequestParts (no body) or FromRequest (with body)
  • Rejection types implement IntoResponse — automatic error responses
⚠️ Body Extractor Last

Axum extractors that consume the body (Json, String, Bytes) MUST be the last handler parameter. Path, Query, State go first. Wrong ordering is a compile error — AI gets this wrong constantly because Actix doesn't have this constraint.

Rule 2: Shared State with State<T>

The rule: 'Use State<T> for shared application state. Define an AppState struct: #[derive(Clone)] struct AppState { db: PgPool, config: AppConfig }. Register with .with_state(state): Router::new().route(...).with_state(app_state). Access in handlers: async fn handler(State(state): State<AppState>) -> impl IntoResponse. AppState must be Clone + Send + Sync + 'static.'

For database pools: 'Pool types (sqlx::PgPool, deadpool::Pool) are already Clone + Send + Sync — put them directly in AppState. Never create connections per request. Never use global static state. State<T> is Axum's DI mechanism — clean, typed, and testable.'

For nested state: 'Use Extension<T> for request-scoped data set by middleware (authenticated user, request ID). Use State<T> for application-scoped data (config, pools, services). Don't confuse the two — State is configured at router level, Extension is set per-request by middleware.'

Rule 3: Router Composition with merge and nest

The rule: 'Build routers modularly: each domain returns a Router. Compose with merge (flat combination) or nest (prefix + scope): let app = Router::new().merge(user_routes()).merge(order_routes()). Use nest for prefixing: Router::new().nest("/api/v1", api_routes()). Each route module is a function returning Router — testable and independent.'

For route definitions: 'Use .route("/path", method_router) for individual routes: Router::new().route("/users", get(list_users).post(create_user)).route("/users/:id", get(get_user).put(update_user).delete(delete_user)). Use get, post, put, delete, patch from axum::routing — method routers compose with chaining.'

For middleware scoping: 'Apply middleware to specific routers with .layer(): let admin = admin_routes().layer(RequireAdmin). Merge into the main router: Router::new().merge(public_routes).merge(admin). tower::Layer-based middleware scopes to the router it's applied to — not global.'

Rule 4: IntoResponse for All Responses

The rule: 'Handlers return impl IntoResponse. Axum converts the return type to an HTTP response. Built-in IntoResponse types: String, &str, Json<T>, (StatusCode, Json<T>), (StatusCode, String), Html<String>, Redirect. For custom responses: implement IntoResponse on your type. Tuple responses set status + body: (StatusCode::CREATED, Json(user)).'

For error responses: 'Define a custom error type: enum AppError { NotFound, Validation(String), Internal(anyhow::Error) }. Implement IntoResponse: fn into_response(self) -> Response { match self { NotFound => (StatusCode::NOT_FOUND, "Not found").into_response(), ... } }. Handlers return Result<Json<T>, AppError> — Axum calls IntoResponse on both Ok and Err.'

AI generates manual Response::builder() construction — verbose and error-prone. IntoResponse trait implementations handle conversion cleanly. A tuple (StatusCode, Json<T>) is the most common pattern — status code + body in one return.

  • Return impl IntoResponse — Axum converts to HTTP response
  • Tuples: (StatusCode::CREATED, Json(data)) — status + body
  • Json<T> for JSON — Html<String> for HTML — Redirect for redirects
  • Custom AppError + IntoResponse — Result<T, AppError> for handlers
  • Never Response::builder() manually — use IntoResponse implementations
💡 Tuple Responses

(StatusCode::CREATED, Json(user)) is the most common Axum response pattern — status code + body in one tuple. No Response::builder() boilerplate. Axum's IntoResponse turns tuples into proper HTTP responses automatically.

Rule 5: Tower Middleware

The rule: 'Axum uses tower middleware — the same middleware system used by tonic (gRPC), hyper, and other tower-based services. Apply with .layer(): Router::new().layer(CorsLayer::permissive()).layer(TraceLayer::new_for_http()). Use tower-http for common middleware: CorsLayer, TraceLayer, CompressionLayer, TimeoutLayer, SetRequestIdLayer.'

For custom middleware: 'Use axum::middleware::from_fn for simple middleware: async fn auth(State(state): State<AppState>, request: Request, next: Next) -> Result<Response, AppError> { ... let response = next.run(request).await; Ok(response) }. Apply with .layer(middleware::from_fn_with_state(state, auth)). This is the simplest pattern — no Service/Layer boilerplate.'

For testing: 'Use axum::test for request simulation: let app = app(); let response = app.oneshot(Request::builder().uri("/users").body(Body::empty()).unwrap()).await.unwrap(). No server needed — test the full router + middleware stack in-process. Use tower::ServiceExt::oneshot for single-request testing.'

ℹ️ Tower = Reusable Middleware

Axum's tower middleware works with any tower-based service — tonic (gRPC), hyper, and custom services. Middleware you write for Axum is reusable across your entire tower-based infrastructure.

Complete Axum Rules Template

Consolidated rules for Axum projects (use alongside Rust language rules).

  • Extractors in handler params: State<T>, Path<T>, Query<T>, Json<T> — body last
  • State<T> for shared state (Clone + Send + Sync) — Extension<T> for request-scoped
  • Router composition: merge for flat, nest for prefixed — .layer() for scoped middleware
  • impl IntoResponse for all returns — (StatusCode, Json<T>) for status + body
  • Custom AppError + IntoResponse — Result<T, AppError> — never unwrap() in handlers
  • tower-http: CorsLayer, TraceLayer, CompressionLayer — from_fn for custom middleware
  • oneshot() for testing — no server, full middleware stack, in-process
  • cargo clippy + cargo test + cargo nextest in CI — tracing for structured logging
CLAUDE.md for Axum (Rust) Web Framework — RuleSync Blog