Skip to content

Dependency injection

You just added a new input parameter to your request handler and, somehow, the framework was able to provide its value at runtime without you having to do anything.
How does that work?

It's all thanks to dependency injection.
Pavex automatically injects the expected input parameters when invoking your handler functions as long as it knows how to construct them.

Constructor registration

Let's zoom in on PathParams: how does the framework know how to construct it?
You need to go back to the Blueprint to find out:

app/src/blueprint.rs
// [...]
pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    ApiKit::new().register(&mut bp);
    telemetry::register(&mut bp);
    // [...]
}

ApiKit is one of Pavex's kits: it bundles together constructors for types that are commonly used when building APIs with Pavex.
In particular, it includes a constructor for PathParams.

A new extractor: UserAgent

Kits give you a head start, but they're not the last stop on your journey: to leverage Pavex to its full potential, you'll soon need to define and register your own constructors.
There's no substitute for hands-on experience: let's design together a brand-new constructor for our demo project to get a better understanding of how it all works.
We only want to greet people who include a User-Agent header in their request(1).

  1. It's an arbitrary requirement, follow along for the sake of the example!

Let's start by defining a new UserAgent type:

app/src/lib.rs
pub use blueprint::blueprint;

mod blueprint;
pub mod configuration;
pub mod routes;
pub mod telemetry;
pub mod user_agent;
app/src/user_agent.rs
pub enum UserAgent {
    /// No `User-Agent` header was provided.
    Unknown,
    /// The value of the `User-Agent` header for the incoming request.
    Known(String),
}

Missing constructor

What if you tried to inject UserAgent into your request handler straight away? Would it work?
Let's find out!

app/src/routes/greet.rs
// [...]
use crate::user_agent::UserAgent;
// [...]
pub fn get(params: PathParams<GreetParams>, user_agent: UserAgent /* (1)! */) -> Response {
    if let UserAgent::Unknown = user_agent {
        return Response::unauthorized().set_typed_body("You must provide a `User-Agent` header");
    }
    // [...]
}
  1. New input parameter!

If you try to build the project now, you'll get an error from Pavex:

ERROR: 
  × I can't find a constructor for `app::user_agent::UserAgent`.
   I need an instance of `app::user_agent::UserAgent` to invoke your request
   handler, `app::routes::greet::get`.
  
       ╭─[app/src/routes/mod.rs:8:1]
     8 │     bp.route(GET, "/api/ping", f!(self::ping::get));
     9 │     bp.route(GET, "/api/greet/:name", f!(self::greet::get));
       ·                                       ──────────┬─────────
       ·       The request handler was registered here ──╯
    10 │ }
       ╰────
       ╭─[app/src/routes/greet.rs:10:1]
    10    11 │ pub fn get(params: PathParams<GreetParams>, user_agent: UserAgent) -> Response {
       ·                                             ──────────┬──────────
       ·        I don't know how to construct an instance of this input parameter
    12 │     if let UserAgent::Unknown = user_agent {
       ╰────
     help: Register a constructor for `app::user_agent::UserAgent`.
     help: Alternatively, use `Blueprint::prebuilt` to add a new input
           parameter of type `app::user_agent::UserAgent` to the (generated)
           `build_application_state`.

The invocation of `pavex [...] generate [...]` exited with a non-zero status code: 1
error: Failed to run `bp`, the code generator for package `server_sdk`

Pavex cannot do miracles, nor does it want to: it only knows how to construct a type if you tell it how to do so.

By the way: this is also your first encounter with Pavex's error messages!
We strive to make them as helpful as possible. If you find them confusing, report it as a bug!

Add a new constructor

To inject UserAgent into your request handler, you need to define a constructor for it.
Constructors, just like request handlers, can take advantage of dependency injection: they can request input parameters that will be injected by the framework at runtime.
Since you need to look at headers, ask for RequestHead as input parameter: the incoming request data, minus the body.

app/src/user_agent.rs
use pavex::http::header::USER_AGENT;
use pavex::request::RequestHead;
// [...]
impl UserAgent {
    pub fn extract(request_head: &RequestHead) -> Self {
        let Some(user_agent) = request_head.headers.get(USER_AGENT) else {
            return Self::Unknown;
        };

        match user_agent.to_str() {
            Ok(s) => Self::Known(s.into()),
            Err(_e) => todo!(),
        }
    }
}

Now register the new constructor with the Blueprint:

app/src/blueprint.rs
// [...]
use pavex::f;
// [...]
pub fn blueprint() -> Blueprint {
    // [...]
    bp.request_scoped(f!(crate::user_agent::UserAgent::extract));
    // [...]
}

In Blueprint::request_scoped you must specify an unambiguous path to the constructor method, wrapped in the f! macro.

Make sure that the project compiles successfully at this point.

Lifecycles

A constructor registered via Blueprint::request_scoped has a request-scoped lifecycle: the framework will invoke a request-scoped constructor at most once per request.

You can register constructors with two other lifecycles: singleton and transient.
Singletons are built once and shared across requests.
Transient constructors, instead, are invoked every time their output type is needed—potentially multiple times for the same request.

UserAgent wouldn't be a good fit as a singleton or a transient constructor:

  • UserAgent depends on the headers of the incoming request. It would be incorrect to mark it as a singleton and share it across requests (and Pavex wouldn't allow it!)
  • You could register UserAgent as transient, but extracting (and parsing) the User-Agent header multiple times would be wasteful. As a request-scoped constructor, it's done once and the outcome is reused.