What's new in axum 0.5

March 31, 2022

Today, we're happy to announce axum version 0.5. axum is an ergonomic and modular web framework built with tokio, tower, and hyper.

0.5 contains lots of new features and I'd like highlight a few of them here.

This also includes new major versions for axum-core, axum-extra, and axum-macros.

The new IntoResponseParts trait

axum has always supported building responses by composing individual parts:

use axum::{
    Json,
    response::IntoResponse,
    http::{StatusCode, HeaderMap},
};
use serde_json::json;

// returns a JSON response
async fn json() -> impl IntoResponse {
    Json(json!({ ... }))
}

// returns a JSON response with a `201 Created` status code and
// a custom header
async fn json_with_status_and_header() -> impl IntoResponse {
    let mut headers = HeaderMap::new();
    headers.insert("x-foo", "custom".parse().unwrap());

    (StatusCode::CREATED, headers, Json(json!({})))
}

However, you couldn't easily provide your own custom response parts. axum had to specifically allow HeaderMap to be included in responses, and you couldn't extend this system with your own types.

The new IntoResponseParts trait fixes that!

For example, we can add our own SetHeader type for setting a single header, and implement IntoResponseParts for it.

use axum::{
    response::{ResponseParts, IntoResponseParts},
    http::{StatusCode, header::{HeaderName, HeaderValue}},
};

struct SetHeader<'a>(&'a str, &'a str);

impl<'a> IntoResponseParts for SetHeader<'a> {
    type Error = StatusCode;

    fn into_response_parts(
        self,
        mut res: ResponseParts,
    ) -> Result<ResponseParts, Self::Error> {
        let name = self.0.parse::<HeaderName>()
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

        let value = self.1.parse::<HeaderValue>()
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

        res.headers_mut().insert(name, value);

        Ok(res)
    }
}

We can now use SetHeader in responses:

use axum::{Json, response::IntoResponse, http::StatusCode};
use serde_json::json;

async fn json_with_status_and_header() -> impl IntoResponse {
    (
        StatusCode::CREATED,
        SetHeader("x-foo", "custom"),
        SetHeader("x-bar", "another custom header"),
        Json(json!({})),
    )
}

IntoResponseParts is also implemented for Extension, making it easy to set response extensions. For example, this can be used to share state with middleware:

use axum::{Extension, Json, response::IntoResponse};
use serde_json::json;

async fn json_extensions() -> impl IntoResponse {
    (
        Extension(some_value),
        Extension(some_other_value),
        Json(json!({})),
    )
}

If including a status code it must be the first element of the tuple and any response body must be the last. This ensures you only set those parts once and don't accidentally override them.

See axum::response for more details.

Cookies

Building on top of IntoResponseParts axum-extra, has a new CookieJar extractor:

use axum_extra::extract::cookie::{CookieJar, Cookie};
use axum::response::IntoResponseParts;

async fn handler(jar: CookieJar) -> impl IntoResponse {
    if let Some(cookie_value) = jar.get("some-cookie") {
        tracing::info!(?cookie_value);
    }

    let updated_jar = jar
        .add(Cookie::new("session_id", "value"))
        .remove(Cookie::named("some-cookie"));

    (updated_jar, "response body...")
}

It also comes in a SignedCookieJar variant that will sign cookies with a key, so you're sure someone hasn't tampered with them.

IntoResponseParts makes this possible without requiring any middleware.

See axum_extra::extract::cookie for more details.

HeaderMap extractor

You've always been able to use HeaderMap as an extractor to access headers from the request. But what you might not realise is that this would implicitly consume the headers, such that other extractors wouldn't be able to access them.

For example, this is subtly broken:

use axum::{http::HeaderMap, extract::Form};

async fn handler(
    headers: HeaderMap,
    form: Form<Payload>,
) {
    // ...
}

Since we run the HeaderMap first, Form would be unable to access them and fail with a 500 Internal Server Error. This was quite surprising, and caused headaches for some users.

However, in axum 0.5 this problem goes away and it just works!

More flexible Router::merge

Router::merge can be used to merge two routers into one. In axum 0.5, it has gotten slightly more flexible, and now accepts any impl Into<Router>. This allows you to have custom ways of constructing Routers, and have them work seamlessly with axum.

One could imagine a way to compose REST and gRPC like so:

let rest_routes = Router::new().route(...);

// with `impl From<GrpcService> for Router`
let grpc_service = GrpcService::new(GrpcServiceImpl::new());

let app = Router::new()
    .merge(rest_routes)
    .merge(grpc_service);

Honorable mentions

The following features weren't new in 0.5, but shipped recently and are worthy of a shout out.

middleware::from_fn

axum uses the tower::Service trait for middleware. However, it can be little daunting to implement, mainly due to the lack of async traits in Rust.

But with axum::middleware::from_fn you can hide all that complexity and use a familiar async function:

use axum::{
    Router,
    http::{Request, StatusCode},
    routing::get,
    response::IntoResponse,
    middleware::{self, Next},
};

async fn my_middleware<B>(
    req: Request<B>,
    next: Next<B>,
) -> impl IntoResponse {
    // transform the request...

    let response = next.run(request).await;

    // transform the response...

    response
}

let app = Router::new()
    .route("/", get(|| async { /* ... */ }))
    // add our middleware function
    .layer(middleware::from_fn(my_middleware));

The documentation for middleware has also been reworked, and goes into more details about the different ways to write middleware, when to pick which approach, how ordering works, and more.

Type-safe routing

In axum-extra, we're experimenting with "type-safe routing". The idea is is to establish a type-safe connection between a path and the corresponding handler.

Previously, it was possible to add a path like /users and apply a Path<u32> extractor, which would always fail at runtime, since the path doesn't contain any parameters.

We can use axum-extra's type-safe routing to prevent that problem at compile-time:

use serde::Deserialize;
use axum::Router;
use axum_extra::routing::{
    TypedPath,
    RouterExt, // for `Router::typed_get`
};

// A type-safe path
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id")]
struct UsersMember {
    id: u32,
}

// A regular handler function that takes `UsersMember` as the
// first argument and thus creates a typed connection between
// this handler and the `/users/:id` path.
async fn users_show(path: UsersMember) {
    tracing::info!(?path.id, "users#show called!");
}

let app = Router::new()
    // Add our typed route to the router.
    //
    // The path will be inferred to `/users/:id` since `users_show`'s
    // first argument is `UsersMember` which implements `TypedPath`
    .typed_get(users_show);

The key here is that our users_show function doesn't have any macros, so IDE integration continues to work great.

See axum_extra::routing::TypedPath for more details.

Updating

axum 0.5 also contains a few breaking changes, but I'd say they're all fairly minor. Don't hesitate to reach out if you're having trouble upgrading or have questions in general! You can find us in the #axum channel in the Tokio Discord server.

ā€” David Pedersen (@davidpdrsn)