Skip to content
rusthacks.dev
Web

Axum 0.8 Changes and How to Migrate Your Routes Explained

Axum 0.8 changes path syntax from :id to {id}, removes the need for async_trait on extractors, tightens Option extractors and requires Sync handlers. Migrate step by step with before and after code.

3 min read
Summarize in AI
Flat illustration for the post “Axum 0.8 Changes and How to Migrate Your Routes Explained”, composed around errors.

Axum 0.8 is a small release for most applications. The headline change is a new path syntax; the rest is cleanup that removes boilerplate. This post lists each change with before and after code so you can migrate in one sitting. The examples target Rust 1.90 with edition 2024, Axum 0.8 and Tokio 1.47.

1. Path syntax: {param} and {*wildcard}

rust
// 0.7
let app = Router::new()
    .route("/users/:id", get(user))
    .route("/files/*path", get(file));

// 0.8
let app = Router::new()
    .route("/users/{id}", get(user))
    .route("/files/{*path}", get(file));

The new form aligns with format! and with OpenAPI, and frees : and * to appear literally in paths. To match a literal brace, double it: /{{literal}}. Using the old syntax panics when the router is built:

Text
thread 'main' panicked at src/main.rs:8:
Invalid route "/users/:id": Path segments must not start with `:`. For capture groups, use `{capture}`. If you meant to literally match a segment starting with a colon, call `without_v07_checks` on the router.

The extractor side is unchanged: Path<u32>, Path<(u32, String)> and Path<MyStruct> work as before, and names in the struct map to the braces.

2. Extractors: drop #[async_trait]

rust
use axum::{extract::FromRequestParts, http::{request::Parts, StatusCode}};

struct ApiKey(String);

// 0.7 needed #[async_trait] here
impl<S: Send + Sync> FromRequestParts<S> for ApiKey {
    type Rejection = StatusCode;

    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
        parts
            .headers
            .get("x-api-key")
            .and_then(|v| v.to_str().ok())
            .map(|k| ApiKey(k.to_owned()))
            .ok_or(StatusCode::UNAUTHORIZED)
    }
}

The traits use native async fn in trait now (async traits then and now). Remove the macro and the async-trait dependency if nothing else uses it. A leftover #[async_trait] usually still compiles but is redundant.

3. Option extractors

rust
// 0.7: Option<T> turned ANY rejection into None
async fn handler(user: Option<Json<Payload>>) {}

// 0.8: Option<T> works only when T: OptionalFromRequest(Parts)
// and a malformed body is now an error, not None

This closes a long-standing footgun: a client sending invalid JSON used to be indistinguishable from sending none. Now Option<Json<T>> gives None only when the Content-Type header is absent, and rejects a bad body. If you relied on the old behavior for your own extractor, implement OptionalFromRequestParts and decide what absent means. If you want the old swallow-everything behavior on a built-in extractor, take Result<T, T::Rejection> and match.

4. Sync requirement

Handlers and services now need to be Sync in addition to Send, mostly relevant if you store a non-Sync value in state or a closure. The fix is to wrap it in a Mutex or use an Arc<Mutex<T>>. A compile error mentioning Sync on a handler points here.

5. Smaller changes

  • Host extractor moved to axum-extra.
  • serve accepts any Listener, so Unix sockets and custom acceptors work without extra glue.
  • WebSocket messages use Utf8Bytes and Bytes instead of String and Vec<u8>; wrap with .into() when constructing a Message::Text.
  • Router::method_not_allowed_fallback lets you customize the response for a path that exists but not for that method.

Migration checklist

  1. Bump axum, axum-extra, tower-http and tokio together; run cargo update and cargo tree -d to check for two versions of http or hyper (duplicate versions).
  2. Search and replace route strings: /:name to /{name}, /*rest to /{*rest}.
  3. Remove #[async_trait] from extractor impls.
  4. Fix Option<...> extractors deliberately: decide what a malformed input should do.
  5. Run the router-building test and the integration tests (integration tests).
Taggedaxum-0-8
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.