Browse documentation
Documentation

Path parameters

Edit this guide on GitHub

Declares a typed path parameter.

Name the parameter as it appears in the URL. The macro converts that name to Pascal case for the generated type.

path_param!(post_id: u64);
// Generates `struct PostId(u64)`.

path_param! only declares the parameter. You add it to a route by writing {post_id} in the handler's path. module_param! makes the same declaration and also sets the module's URL segment to {post_id}.

Matching the URL

Add the parameter name in braces to the route path, such as {post_id}. With module routing, use a path starting with ./ to add it after the module's path.

// src/app/posts.rs serves /posts/{post_id}.
path_param!(post_id: u64);

#[page("./{post_id}")]
async fn post() -> Result<impl View> {
    Ok(view! { "post" })
}

You can declare several parameters in one module and use them in your handler paths. path_param! leaves the module's URL segment unchanged.

Reading a parameter that the matched route did not capture panics.

Reading one segment

path_param::<T>(cx) reads the parameter from the matched route. A declaration with : Type parses the segment with FromStr and memoizes the result for the request.

// src/app/posts.rs
path_param!(post_id: u64);

#[page("./{post_id}")]
async fn post(cx: &Cx) -> Result<impl View> {
    let post_id: &u64 = path_param::<PostId>(cx).ok_or_not_found()?;
    Ok(view! { "post " (post_id) })
}

Without a type, path_param::<Slug>(cx) returns the percent-decoded segment as &str without allocating or failing.

// src/app/posts.rs
path_param!(slug);

#[page("./{slug}")]
async fn post(cx: &Cx) -> Result<impl View> {
    let slug: &str = path_param::<Slug>(cx);
    Ok(view! { "slug " (slug) })
}

The unparsed declaration generates struct Slug<T: AsRef<str> = String>(T). String is the default type argument, so type positions can use Slug; construction accepts owned or borrowed strings.

Failing with an error response

error = ... maps a parse failure to a router error, so a handler can use ?.

// src/app/posts.rs
path_param!(post_id: u64, error = not_found);

#[page("./{post_id}")]
async fn post(cx: &Cx) -> Result<impl View> {
    let post_id = path_param::<PostId>(cx)?;
    Ok(view! { "post " (post_id) })
}

The supported forms mirror the router's error constructors:

A bare error = bad_request uses invalid value for path parameter "post_id".

Without error = ..., the reader returns Result<&T, &<T as FromStr>::Err> and the call site chooses a response with RouterErrorExt.

An unparsed parameter cannot use error because it cannot fail.

Visibility and construction

The declared visibility applies to the generated type and its field.

path_param!(pub post_id: u64);
path_param!(pub slug);
path_param!(pub *ids: u32);

let id = PostId(42);
let borrowed = Slug("first-post");
let owned: Slug = Slug("first-post".to_owned());
let ids = Ids(vec![1, 2, 3]);

Keep the declaration private when only descendant modules read it. Use the narrowest visibility needed by code that names or constructs the type, such as pub(super), pub(crate), or pub.

Catch-all parameters

Prefix the name with * to capture the remaining path as separate decoded segments. A catch-all must be the last served segment and matches at least one segment.

// src/app/docs.rs
path_param!(*doc_path);

#[page("./{*doc_path}")]
async fn document(cx: &Cx) -> Result<impl View> {
    let path: CatchAllSegments<'_> = path_param::<DocPath>(cx);
    let path = path.collect::<std::path::PathBuf>();
    Ok(view! { (path.display().to_string()) })
}

CatchAllSegments yields one decoded &str per URL segment. For /docs/api%2Frouter/start, it yields "api/router" and "start"; the encoded slash stays inside the first segment.

A typed catch-all parses each segment and returns a memoized slice.

// src/app/archive.rs
path_param!(*ids: u32, error = bad_request);

#[page("./{*ids}")]
async fn archive(cx: &Cx) -> Result<impl View> {
    let ids: &[u32] = path_param::<Ids>(cx)?;
    Ok(view! { (format!("{ids:?}")) })
}

The type after : is the type of one segment, so this declaration generates struct Ids(Vec<u32>). Without error = ..., the reader returns Result<&[u32], &<u32 as FromStr>::Err>; it returns the first parse error without a segment index.

A bare error = bad_request includes the zero-based failing segment index. For /archive/1/x/3, its description is invalid value for path parameter "ids" at segment 1.

An unparsed catch-all accepts any IntoIterator whose items implement AsRef<str>. For example, path_param!(pub *doc_path) accepts DocPath(["guide", "start"]) and defaults to DocPath<Vec<String>> in type positions.

Building URLs

Pass the generated type to href! to fill the matching parameter in a handler's URL:

// src/app.rs
mod posts {
    use super::*;

    path_param!(pub post_id: u64);

    #[page("./{post_id}")]
    pub async fn post() -> Result<impl View> {
        Ok(view! { "post" })
    }
}

mod docs {
    use super::*;

    path_param!(pub *doc_path);

    #[page("./{*doc_path}")]
    pub async fn document() -> Result<impl View> {
        Ok(view! { "doc" })
    }
}

#[page]
async fn home() -> Result<impl View> {
    Ok(view! {
        // /posts/1
        <a href=(href!(posts::post, posts::PostId(1)))>"The first post"</a>
        // /docs/guides/getting%20started
        <a href=(href!(docs::document, docs::DocPath(["guides", "getting started"])))>"Guides"</a>
    })
}

Each argument must have the name expected by its path parameter. For example, filling {post_id} with a parameter named slug panics.

Each segment is written with Display and percent-encoded, so a value stays inside the segment it fills: Slug("a/b") fills its one segment as a%2Fb. A catch-all contributes one segment per element, so the separators between them are the only / it adds.

Empty values, ., and .. panic because browsers treat them as path structure rather than segment content.

href takes the same values as a tuple, for a URL built outside a macro.

Requirements