Skip to content

Cookbook

This cookbook contains a collection of reference examples for Pavex's dependency injection framework.
It covers the registration syntax for common use cases (free functions, methods) as well as more advanced ones (trait methods, generics, etc.).

Use it a reference in your day-to-day Pavex development if you're not sure of the syntax for a particular use case.

More than constructors

All the examples register constructors, but the very same f! invocations can be used to register request handlers, error handlers, error observers and middlewares.

Unambiguous paths

In all the cases described in this cookbook, we'll talk about unambiguous paths. They allow Pavex to identify with certainty the callable you want to register.
The format differs between local items and items imported from a dependency of your crate.

Local items

If the item you want to register is defined in the current crate, you can use three different formats for its unambiguous path:

  • A path prefixed with crate, spelling out where the item is located with respect to the root of the crate.
    //! You can use `crate::my_module::handler` as an unambiguous path
    //! for the `handler` function from anywhere in your crate.
    
    pub mod my_module {
       pub fn handler() {
          // [...]
       }
    }
    
  • A path prefixed with self, spelling out where the item is located with respect to the root of the current module.
    pub mod my_module {
       //! From inside `my_module`, you can use `self::handler` as an unambiguous path
       //! for the `handler` function.
       pub fn handler() {
          // [...]
       }
    }
    
  • A path prefixed with super, spelling out where the item is located with respect to the parent of the current module.
    pub fn handler() {
       // [...]
    }
    
    pub mod my_module {
       //! From inside `my_module`, you can use `super::handler` as an unambiguous path
       //! for the `handler` function.
    }
    

The three formats are equivalent for Pavex. In practice, paths prefixed with self or super often end up being shorter—prefer them if you want to make your registration code terser.

From a dependency

If the item is defined in a dependency of your crate, you must use its absolute path, starting the name of the crate it is defined into. E.g. reqwest::Client, if reqwest is one of your dependencies.

Free functions

Free functions are the simplest case.
You pass an unambiguous path to the f! macro as input.

src/functions/blueprint.rs
use pavex::blueprint::Blueprint;
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.request_scoped(f!(super::extract));
    // [...]
}
src/functions/constructor.rs
use crate::User;

pub async fn extract() -> User {
    // The constructor logic goes here
    // [...]
}

Static methods

Static methods (1) behave exactly like free functions: you provide an unambiguous path to the f! macro.

  1. A static method is a method that doesn't take self (or one of its variants) as an input parameter.
src/static_methods/blueprint.rs
use pavex::blueprint::Blueprint;
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.request_scoped(f!(crate::User::extract));
    // [...]
}
src/static_methods/constructor.rs
use crate::User;

impl User {
    pub async fn extract() -> User {
        // The constructor logic goes here
        // [...]
    }
}

Non-static methods

On the surface, non-static methods are registered in the same way as static methods: by passing an unambiguous path to the f! macro.

src/non_static_methods/blueprint.rs
use pavex::blueprint::Blueprint;
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.request_scoped(f!(super::UserStore::retrieve));
    // [...]
}
src/non_static_methods/constructor.rs
use pavex::request::RequestHead;

use crate::User;

pub struct UserStore(/* ... */);

impl UserStore {
    // [...]
    pub async fn retrieve(&self, request_head: &RequestHead) -> User {
        // The constructor logic goes here
        // [...]
    }
}

However, there's a catch: self counts as a dependency!
When registering a non-static method, you need to make sure to also register a constructor for the type of self.

src/non_static_methods/blueprint.rs
use pavex::blueprint::Blueprint;
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    // [...]
    bp.request_scoped(f!(super::UserStore::new));
    // [...]
}
src/non_static_methods/constructor.rs
// [...]
impl UserStore {
    pub fn new() -> Self {
        // The constructor logic goes here
        // [...]
    }
    // [...]
}

Trait methods

The syntax for trait methods (1) is a bit more complex: you need to use the fully qualified syntax for function calls1.

  1. An inherent method is defined directly on a type (e.g. Vec::new). A trait method is part of trait definition (e.g. Iterator::next) and it's available on a type if that type implements the trait.
src/trait_methods/blueprint.rs
use pavex::blueprint::Blueprint;
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.request_scoped(f!(<crate::User as super::WithId>::id));
    // [...]
}
src/trait_methods/constructor.rs
pub trait WithId {
    fn id(&self) -> u64;
}

impl WithId for crate::User {
    fn id(&self) -> u64 {
    // [...]
    }
}

Generics

Output-driven generics

A generic parameter is output-driven if it appears in the output type of a constructor.

src/output/constructor.rs
use pavex::request::body::errors::ExtractJsonBodyError;
use pavex::request::body::BufferedBody;

pub fn parse<T>(body: BufferedBody) -> JsonBody<T> {
// [...]
}

If the constructor is fallible, the generic parameter must appear in type of the Ok variant to qualify as output-driven.

src/output/constructor.rs
// [...]
pub fn fallible_parse<T>(body: BufferedBody) -> Result<JsonBody<T>, ExtractJsonBodyError> {
// [...]
}

If all generic parameters are output-driven, you can register the constructor as if it wasn't generic. Pavex will automatically infer the generic parameters based on the scenarios where the constructor is used.

src/output/blueprint.rs
use pavex::blueprint::Blueprint;
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.request_scoped(f!(super::parse));
    // [...]
}

Input-driven generics

A generic parameter is input-driven if it isn't output-driven, i.e. it doesn't appear in the output type of a constructor.

src/input/constructor.rs
use pavex::request::body::JsonBody;

pub fn length<T>(body: JsonBody<T>) -> u64 {
// [...]
}

If all generic parameters are input-driven, you need to explicitly specify the concrete type for each generic parameter when registering the constructor.

src/input/blueprint.rs
use pavex::blueprint::Blueprint;
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.request_scoped(f!(super::length::<super::GreetBody>));
    // [...]
}

Mixed generics

If a constructor has both input-driven and output-driven generic parameters, you need to explicitly specify the concrete type for all generic parameters when registering the constructor.


  1. Check out the relevant RFC if you're curious.