pub trait TypedPath: Display {
    const PATH: &'static str;

    fn to_uri(&self) -> Uri { ... }
}
Expand description

A type safe path.

This is used to statically connect a path to its corresponding handler using RouterExt::typed_get, RouterExt::typed_post, etc.

Example

use serde::Deserialize;
use axum::{Router, extract::Json};
use axum_extra::routing::{
    TypedPath,
    RouterExt, // for `Router::typed_*`
};

// A type safe route with `/users/:id` as its associated 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.
//
// The `TypedPath` must be the first argument to the function.
async fn users_show(
    UsersMember { id }: UsersMember,
) {
    // ...
}

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)
    .typed_post(users_create)
    .typed_delete(users_destroy);

#[derive(TypedPath)]
#[typed_path("/users")]
struct UsersCollection;

#[derive(Deserialize)]
struct UsersCreatePayload { /* ... */ }

async fn users_create(
    _: UsersCollection,
    // Our handlers can accept other extractors.
    Json(payload): Json<UsersCreatePayload>,
) {
    // ...
}

async fn users_destroy(_: UsersCollection) { /* ... */ }

Using #[derive(TypedPath)]

While TypedPath can be implemented manually, it’s highly recommended to derive it:

use serde::Deserialize;
use axum_extra::routing::TypedPath;

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

The macro expands to:

  • A TypedPath implementation.
  • A FromRequest implementation compatible with RouterExt::typed_get, RouterExt::typed_post, etc. This implementation uses Path and thus your struct must also implement serde::Deserialize, unless it’s a unit struct.
  • A Display implementation that interpolates the captures. This can be used to, among other things, create links to known paths and have them verified statically. Note that the Display implementation for each field must return something that’s compatible with its Deserialize implementation.

Additionally the macro will verify the captures in the path matches the fields of the struct. For example this fails to compile since the struct doesn’t have a team_id field:

use serde::Deserialize;
use axum_extra::routing::TypedPath;

#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id/teams/:team_id")]
struct UsersMember {
    id: u32,
}

Unit and tuple structs are also supported:

use serde::Deserialize;
use axum_extra::routing::TypedPath;

#[derive(TypedPath)]
#[typed_path("/users")]
struct UsersCollection;

#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id")]
struct UsersMember(u32);

Percent encoding

The generated Display implementation will automatically percent-encode the arguments:

use serde::Deserialize;
use axum_extra::routing::TypedPath;

#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id")]
struct UsersMember {
    id: String,
}

assert_eq!(
    UsersMember {
        id: "foo bar".to_string(),
    }.to_string(),
    "/users/foo%20bar",
);

Customizing the rejection

By default the rejection used in the FromRequest implemetation will be PathRejection.

That can be customized using #[typed_path("...", rejection(YourType))]:

use serde::Deserialize;
use axum_extra::routing::TypedPath;
use axum::{
    response::{IntoResponse, Response},
    extract::rejection::PathRejection,
};

#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id", rejection(UsersMemberRejection))]
struct UsersMember {
    id: String,
}

struct UsersMemberRejection;

// Your rejection type must implement `From<PathRejection>`.
//
// Here you can grab whatever details from the inner rejection
// that you need.
impl From<PathRejection> for UsersMemberRejection {
    fn from(rejection: PathRejection) -> Self {
        // ...
    }
}

// Your rejection must implement `IntoResponse`, like all rejections.
impl IntoResponse for UsersMemberRejection {
    fn into_response(self) -> Response {
        // ...
    }
}

The From<PathRejection> requirement only applies if your typed path is a struct with named fields or a tuple struct. For unit structs your rejection type must implement Default:

use axum_extra::routing::TypedPath;
use axum::response::{IntoResponse, Response};

#[derive(TypedPath)]
#[typed_path("/users", rejection(UsersCollectionRejection))]
struct UsersCollection;

#[derive(Default)]
struct UsersCollectionRejection;

impl IntoResponse for UsersCollectionRejection {
    fn into_response(self) -> Response {
        // ...
    }
}

Required Associated Constants

The path with optional captures such as /users/:id.

Provided Methods

Convert the path into a Uri.

Panics

The default implementation parses the required Display implemetation. If that fails it will panic.

Using #[derive(TypedPath)] will never result in a panic since it percent-encodes arguments.

Implementors