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
    // [...]
    bp.import(from![
        // Local components, defined in this crate
        crate,
        // Components defined in the `pavex` crate,
        // by the framework itself.
        pavex,
    ]);

We're importing all the constructors defined in the pavex crate. In particular, this includes a constructor for PathParams.

A new extractor: UserAgent

The framework gives you a head start with its built-in components, but they're not enough: to build a real application with Pavex, 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)
           `ApplicationState::new` method.

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, file a bug report.

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 {
    #[request_scoped]
    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!(),
        }
    }

The #[request_scoped] annotation tells Pavex that the new method is a constructor.

Try to recompile the project—there should be no error now.
The new constructor was picked up immediately because our Blueprint is configured to import all constructors defined in the current crate:

app/src/blueprint.rs
    // [...]
    bp.import(from![
        // Local components, defined in this crate
        crate,
        // Components defined in the `pavex` crate,
        // by the framework itself.
        pavex,
    ]);

Lifecycles

A constructor registered via #[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.