Skip to content

Query parameters

In REST APIs, the query is often used to encode data.
For example, in /search?sorted=true, the query is sorted=true and it's used to encode a sorted variable set to true.

Those variables are called query parameters. You can extract them using QueryParams<T>.

Registration

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

src/blueprint.rs
use pavex::blueprint::Blueprint;
use pavex::request::query::QueryParams;

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

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

Overview

Let's keep using /search?sorted=true as an example.

You can parse the value for sorted by injecting QueryParams<T> in your handler:

src/query_params/routes.rs
use pavex::http::StatusCode;
use pavex::request::query::QueryParams;

#[derive(serde::Deserialize)]
pub struct SearchParams {
    pub sorted: bool,
}

pub fn handler(params: &QueryParams<SearchParams>) -> StatusCode {
    if params.0.sorted {
        println!("The search is sorted");
    }
    // [...]
}

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

Fields names

QueryParams<T> is a generic wrapper around a struct that models the query parameters for a given path.
All struct fields must be named after the query parameters you want to extract.

In our example, the query parameter is named sorted.
Our extraction type, SearchParams, must have a matching field named sorted.

src/query_params/routes.rs
// [...]
pub struct SearchParams {
    pub sorted: bool,
}

Deserialization

The newly defined struct must be deserializable—i.e. it must implement the serde::Deserialize trait.
You can derive serde::Deserialize in most cases.

src/query_params/routes.rs
// [...]
#[derive(serde::Deserialize)]
pub struct SearchParams {
    pub sorted: bool,
}

Parsing

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

In our example, we expect the sorted parameter to be a boolean.
We could set the field type for sorted to String and then parse it into a boolean 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 boolean query parameter.
We can skip all that boilerplate by setting the field type to bool directly, and let Pavex do the parsing for us:

src/query_params/routes.rs
// [...]
#[derive(serde::Deserialize)]
pub struct SearchParams {
    pub sorted: bool,
}

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

Supported field types

All "value" types (booleans, numbers, strings, etc.) can be used as fields in your query struct (i.e. the T in QueryParams<T>).

Sequences

There is no standard way to represent sequences in query parameters.
Pavex supports the form style, as specified by OpenAPI:

#[derive(serde::Deserialize)]
pub struct SearchParams {
    // This will parse `?country_id=1&country_id=2&country_id=3`
    // into a vector `vec![1, 2, 3]`.  
    //
    // Pavex does not perform any pluralization, therefore you must use
    // `serde`'s rename attribute if you want to use a pluralized name
    // as struct field but a singularized name in the query string.
    #[serde(rename = "country_id")]
    country_ids: Vec<u32>
}

Another common way to represent sequences in query parameters is to use brackets. E.g. ?country_ids[]=1&country_ids[]=2&country_ids[]=3.

You can use the serde's rename attribute to support the bracket style:

#[derive(serde::Deserialize)]
pub struct SearchParams {
    // This will parse `?country_ids[]=1&country_ids[]=2&country_ids[]=3`
    // into a vector `vec![1, 2, 3]`.  
    #[serde(rename = "country_ids[]")]
    country_ids: Vec<u32>
}

Unsupported field types

QueryParams<T> doesn't support deserializing nested structures. For example, the following can't be deserialized from the wire using QueryParams<T>:

#[derive(serde::Deserialize)]
pub struct SearchParams {
    address: Address
}

#[derive(serde::Deserialize)]
pub struct Address {
    street: String,
    city: String,
}

If you need to deserialize nested structures from query parameters, you might want to look into writing your own extractor on top of serde_qs.

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 query parameters.
Pavex supports this use case—you can borrow from the request's query.

Percent-encoding

It is not always possible to avoid allocations when handling query parameters.
Query 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 query parameter, you must encode it as %20. A string like John Doe becomes John%20Doe when percent-encoded.

QueryParams<T> automatically decodes percent-encoded strings for you. But that comes at a cost: Pavex must allocate a new String if the route 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 route parameter is percent-encoded, but you tried to use &str as its field type.