Skip to content

Path parameters

In REST APIs, the path is often used to identify a resource. For example, in https://example.com/users/123, the path is /users/123 and the resource is the user with ID 123.

Those dynamic path segments are called path parameters. In Pavex, you must declare the path parameters for a given path in the route definition—see Path parameters for more details. You then use PathParams<T> to extract the parameters from the incoming request.

Registration

To use PathParams<T> in your application you need to register a constructor for it. You can use PathParams::register to register its default constructor and error handler:

src/blueprint.rs
use pavex::blueprint::Blueprint;
use pavex::request::path::PathParams;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    PathParams::register(&mut bp);
    // [...]
}

If you're using the default ApiKit, you don't need to register a constructor for PathParams<T> manually: it's already included in the kit.

Overview

Let's keep using https://example.com/users/123 as an example. To extract 123 from the path, you register /users/{id} as the path pattern for that route.

src/route_params/blueprint.rs
use pavex::blueprint::router::GET;
use pavex::blueprint::Blueprint;
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.route(GET, "/users/{id}" /* (1)! */, f!(super::handler));
    bp
}
  1. The path pattern for the route.

You can then access the id value for an incoming request by injecting PathParams<T> in your handler:

src/route_params/routes.rs
use pavex::http::StatusCode;
use pavex::request::path::PathParams;

#[PathParams]
pub struct GetUserParams {
    pub id: u64,
}

pub fn handler(params: &PathParams<GetUserParams>) -> StatusCode {
    println!("The user id is {}", params.0.id);
    StatusCode::OK
}

There are a few moving parts here. Let's break them down!

Fields names

PathParams<T> is a generic wrapper around a struct1 that models the path parameters for a given path. All struct fields must be named after the path parameters declared in the path pattern2.

In our example, the path pattern is /users/{id}. Our extraction type, GetUserParams, must have a matching field named id.

src/route_params/routes.rs
// [...]
pub struct GetUserParams {
    pub id: u64,
}

Deserialization

The newly defined struct must be deserializable—i.e. it must implement the serde::Deserialize trait. The #[PathParams] attribute macro will automatically derive serde::Deserialize for you. Alternatively, you can derive or implement serde::Deserialize directly.

src/route_params/routes.rs
// [...]
#[PathParams]
pub struct GetUserParams {
    pub id: u64,
}

If you rely on #[PathParams], Pavex can perform more advanced checks at compile time3 (e.g. detect unsupported types).

Parsing

From a protocol perspective, all path parameters are strings. From an application perspective, you might want to enforce stricter constraints.

In our example, we expect id parameter to be a number. We could set the field type for id to String and then parse it into a number in the handler; however, that's going to get tedious if we need to do it every single time we want to work with a numeric path parameter. We can skip all that boilerplate by setting the field type to u64 directly, and let Pavex do the parsing for us:

src/route_params/routes.rs
// [...]
#[PathParams]
pub struct GetUserParams {
    pub id: u64,
}

Everything works as expected because u64 implements the serde::Deserialize trait.

Unsupported field types

Path parameters are best used to encode values, such as numbers, strings, or dates. There is no standard way to encode more complex types such as collections (e.g. Vec<T>, tuples) in a path parameter. As a result, Pavex doesn't support them.

Pavex will do its best to catch unsupported types at compile time, but it's not always possible.

Avoiding allocations

If you want to squeeze out the last bit of performance from your application, you can try to avoid heap memory allocations when extracting string-like path parameters. Pavex supports this use case—you can borrow from the request's path.

Percent-encoding

It is not always possible to avoid allocations when handling path parameters. Path parameters must comply with the restriction of the URI specification: you can only use a limited set of characters. If you want to use a character not allowed in a URI, you must percent-encode it. For example, if you want to use a space in a path parameter, you must encode it as %20. A string like John Doe becomes John%20Doe when percent-encoded.

PathParams<T> automatically decodes percent-encoded strings for you. But that comes at a cost: Pavex must allocate a new String if the path parameter is percent-encoded.

Cow

We recommend using Cow<'_, str> as your field type for string-like parameters. It borrows from the request's path if possible, it allocates a new String if it can't be avoided.

Cow<'_, str> strikes a balance between performance and robustness: you don't have to worry about a runtime error if the path parameter is percent-encoded, but you tried to use &str as its field type.

Design considerations

Pavex wants to enable local reasoning. It should be easy to understand what each extracted path parameter represents. Structs with named fields are ideal in this regard: by looking at the field name you can immediately understand which path parameter is being extracted. The same is not true for other types, e.g. (String, u64, u32), where you have to go and check the route's path pattern to understand what each entry represents.

use pavex::request::path::PathParams;

// This is self-documenting ✅
// No need to check the route's path pattern to understand 
// what each field represents.
#[PathParams]
pub struct Room {
    home_id: u32,
    room_id: u32,
    street_id: u32,
}

pub fn get_room(params: &PathParams<Room>) -> String {
    // [...]
}

// This isn't self-documenting ❌
// What does the second u32 represent? The room id? The street id?
// Impossible to tell without checking the route's path template.
pub fn get_room_tuple(params: &PathParams<(u32, u32, u32)>) -> String {
    // [...]
}

For this reason, Pavex does not support the following types as T in PathParams<T>:

  • tuples, e.g. (u32, String)
  • tuple structs, e.g. struct HomeId(u32, String)
  • unit structs, e.g. struct HomeId
  • newtypes, e.g. struct HomeId(MyParamsStruct)
  • sequence-like or map-like types, e.g. Vec<String> or HashMap<String, String>
  • enums

  1. Pavex made a deliberate choice of not supporting tuples or other sequence-like types for extracting path parameters. Check out the API reference to learn more about the rationale behind this decision. 

  2. If a field name doesn't match a path parameter name, Pavex will detect it at compile time and return an error. No more runtime errors because you misspelled a field name! 

  3. Check the documentation for StructuralDeserialize if you want to know more about the underlying mechanism.