pub struct Blueprint { /* private fields */ }
Expand description
The starting point for building an application with Pavex.
§Guide
Check out the “Project structure” section of
Pavex’s guide for more details on the role of Blueprint
in Pavex applications.
§Overview
A blueprint defines the runtime behaviour of your application.
It keeps track of:
- route handlers, registered via
Blueprint::route
- constructors, registered via
Blueprint::constructor
- wrapping middlewares, registered via
Blueprint::wrap
- fallback handlers, registered via
Blueprint::fallback
You can also choose to decompose your overall application into smaller sub-components,
taking advantage of Blueprint::nest
and Blueprint::nest_at
.
The information encoded in a blueprint can be serialized via Blueprint::persist
and passed
as input to Pavex’s CLI to generate the application’s server SDK.
Implementations§
source§impl Blueprint
impl Blueprint
sourcepub fn route(
&mut self,
method_guard: MethodGuard,
path: &str,
callable: RawIdentifiers,
) -> RegisteredRoute<'_>
pub fn route( &mut self, method_guard: MethodGuard, path: &str, callable: RawIdentifiers, ) -> RegisteredRoute<'_>
Register a request handler to be invoked when an incoming request matches the specified route.
If a request handler has already been registered for the same route, it will be overwritten.
§Guide
Check out the “Routing” section of Pavex’s guide for a thorough introduction to routing in Pavex applications.
§Example
use pavex::{f, blueprint::{Blueprint, router::GET}};
use pavex::{request::RequestHead, response::Response};
fn my_handler(request_head: &RequestHead) -> Response {
// [...]
}
let mut bp = Blueprint::new();
bp.route(GET, "/path", f!(crate::my_handler));
sourcepub fn prebuilt(&mut self, type_: RawIdentifiers) -> RegisteredPrebuiltType<'_>
pub fn prebuilt(&mut self, type_: RawIdentifiers) -> RegisteredPrebuiltType<'_>
Register a type to be used as input parameter to the (generated) build_application_state
function.
§Guide
Check out the “Dependency injection” section of Pavex’s guide for a thorough introduction to dependency injection in Pavex applications.
sourcepub fn constructor(
&mut self,
callable: RawIdentifiers,
lifecycle: Lifecycle,
) -> RegisteredConstructor<'_>
pub fn constructor( &mut self, callable: RawIdentifiers, lifecycle: Lifecycle, ) -> RegisteredConstructor<'_>
Register a constructor.
If a constructor for the same type has already been registered, it will be overwritten.
§Guide
Check out the “Dependency injection” section of Pavex’s guide for a thorough introduction to dependency injection in Pavex applications.
§Example
use pavex::f;
use pavex::blueprint::{Blueprint, constructor::Lifecycle};
fn logger(log_level: LogLevel) -> Logger {
// [...]
}
let mut bp = Blueprint::new();
bp.constructor(f!(crate::logger), Lifecycle::Transient);
sourcepub fn singleton(
&mut self,
callable: RawIdentifiers,
) -> RegisteredConstructor<'_>
pub fn singleton( &mut self, callable: RawIdentifiers, ) -> RegisteredConstructor<'_>
Register a constructor with a singleton lifecycle.
It’s a shorthand for Blueprint::constructor
—refer to its documentation for
more information on dependency injection in Pavex.
§Example
use pavex::f;
use pavex::blueprint::Blueprint;
fn logger(log_level: LogLevel) -> Logger {
// [...]
}
let mut bp = Blueprint::new();
bp.singleton(f!(crate::logger));
// ^ is equivalent to:
// bp.constructor(f!(crate::logger), Lifecycle::Singleton));
sourcepub fn request_scoped(
&mut self,
callable: RawIdentifiers,
) -> RegisteredConstructor<'_>
pub fn request_scoped( &mut self, callable: RawIdentifiers, ) -> RegisteredConstructor<'_>
Register a constructor with a request-scoped lifecycle.
It’s a shorthand for Blueprint::constructor
—refer to its documentation for
more information on dependency injection in Pavex.
§Example
use pavex::f;
use pavex::blueprint::Blueprint;
fn logger(log_level: LogLevel) -> Logger {
// [...]
}
let mut bp = Blueprint::new();
bp.request_scoped(f!(crate::logger));
// ^ is equivalent to:
// bp.constructor(f!(crate::logger), Lifecycle::RequestScoped));
sourcepub fn transient(
&mut self,
callable: RawIdentifiers,
) -> RegisteredConstructor<'_>
pub fn transient( &mut self, callable: RawIdentifiers, ) -> RegisteredConstructor<'_>
Register a constructor with a transient lifecycle.
It’s a shorthand for Blueprint::constructor
—refer to its documentation for
more information on dependency injection in Pavex.
§Example
use pavex::f;
use pavex::blueprint::Blueprint;
fn logger(log_level: LogLevel) -> Logger {
// [...]
}
let mut bp = Blueprint::new();
bp.transient(f!(crate::logger));
// ^ is equivalent to:
// bp.constructor(f!(crate::logger), Lifecycle::Transient));
sourcepub fn wrap(
&mut self,
callable: RawIdentifiers,
) -> RegisteredWrappingMiddleware<'_>
pub fn wrap( &mut self, callable: RawIdentifiers, ) -> RegisteredWrappingMiddleware<'_>
Register a wrapping middleware.
§Guide
Check out the “Middleware” section of Pavex’s guide for a thorough introduction to middlewares in Pavex applications.
§Example: a timeout wrapper
use pavex::{f, blueprint::Blueprint, middleware::Next, response::Response};
use std::future::{IntoFuture, Future};
use std::time::Duration;
use tokio::time::{timeout, error::Elapsed};
pub async fn timeout_wrapper<C>(next: Next<C>) -> Result<Response, Elapsed>
where
C: Future<Output = Response>
{
timeout(Duration::from_secs(2), next.into_future()).await
}
pub fn api() -> Blueprint {
let mut bp = Blueprint::new();
// Register the wrapping middleware against the blueprint.
bp.wrap(f!(crate::timeout_wrapper));
// [...]
bp
}
sourcepub fn post_process(
&mut self,
callable: RawIdentifiers,
) -> RegisteredPostProcessingMiddleware<'_>
pub fn post_process( &mut self, callable: RawIdentifiers, ) -> RegisteredPostProcessingMiddleware<'_>
Register a post-processing middleware.
§Guide
Check out the “Middleware” section of Pavex’s guide for a thorough introduction to middlewares in Pavex applications.
§Example: a logging middleware
use pavex::{f, blueprint::Blueprint, response::Response};
use pavex_tracing::{
RootSpan,
fields::{http_response_status_code, HTTP_RESPONSE_STATUS_CODE}
};
pub fn response_logger(response: Response, root_span: &RootSpan) -> Response
{
root_span.record(
HTTP_RESPONSE_STATUS_CODE,
http_response_status_code(&response),
);
response
}
pub fn api() -> Blueprint {
let mut bp = Blueprint::new();
// Register the post-processing middleware against the blueprint.
bp.post_process(f!(crate::response_logger));
// [...]
bp
}
sourcepub fn pre_process(
&mut self,
callable: RawIdentifiers,
) -> RegisteredPreProcessingMiddleware<'_>
pub fn pre_process( &mut self, callable: RawIdentifiers, ) -> RegisteredPreProcessingMiddleware<'_>
Register a pre-processing middleware.
§Guide
Check out the “Middleware” section of Pavex’s guide for a thorough introduction to middlewares in Pavex applications.
§Example: path normalization
use pavex::{f, blueprint::Blueprint, response::Response};
use pavex::middleware::Processing;
use pavex::http::{HeaderValue, header::LOCATION};
use pavex::request::RequestHead;
/// If the request path ends with a `/`,
/// redirect to the same path without the trailing `/`.
pub fn redirect_to_normalized(request_head: &RequestHead) -> Processing
{
let Some(normalized_path) = request_head.target.path().strip_suffix('/') else {
// No need to redirect, we continue processing the request.
return Processing::Continue;
};
let location = HeaderValue::from_str(normalized_path).unwrap();
let redirect = Response::temporary_redirect().insert_header(LOCATION, location);
// Short-circuit the request processing pipeline and return the redirect response
// to the client without invoking downstream middlewares and the request handler.
Processing::EarlyReturn(redirect)
}
pub fn api() -> Blueprint {
let mut bp = Blueprint::new();
// Register the pre-processing middleware against the blueprint.
bp.pre_process(f!(crate::redirect_to_normalized));
// [...]
bp
}
sourcepub fn nest_at(&mut self, prefix: &str, blueprint: Blueprint)
pub fn nest_at(&mut self, prefix: &str, blueprint: Blueprint)
Nest a Blueprint
under the current Blueprint
(the parent), adding a common prefix to all the new routes.
§Routes
prefix
will be prepended to all the routes coming from the nested blueprint.
prefix
must be non-empty and it must start with a /
.
If you don’t want to add a common prefix, check out Blueprint::nest
.
§Trailing slashes
prefix
can’t end with a trailing /
.
This would result in routes with two consecutive /
in their paths—e.g.
/prefix//path
—which is rarely desirable.
If you actually need consecutive slashes in your route, you can add them explicitly to
the path of the route registered in the nested blueprint:
use pavex::f;
use pavex::blueprint::{Blueprint, router::GET};
fn app() -> Blueprint {
let mut bp = Blueprint::new();
bp.nest_at("/api", api_bp());
bp
}
fn api_bp() -> Blueprint {
let mut bp = Blueprint::new();
// This will match `GET` requests to `/api//path`.
bp.route(GET, "//path", f!(crate::handler));
bp
}
§Constructors
Constructors registered against the parent blueprint will be available to the nested
blueprint—they are inherited.
Constructors registered against the nested blueprint will not be available to other
sibling blueprints that are nested under the same parent—they are private.
Check out the example below to better understand the implications of nesting blueprints.
§Visibility
use pavex::f;
use pavex::blueprint::{Blueprint, router::GET};
use pavex::blueprint::constructor::Lifecycle;
fn app() -> Blueprint {
let mut bp = Blueprint::new();
bp.constructor(f!(crate::db_connection_pool), Lifecycle::Singleton);
bp.nest(home_bp());
bp.nest(user_bp());
bp
}
/// All property-related routes and constructors.
fn home_bp() -> Blueprint {
let mut bp = Blueprint::new();
bp.route(GET, "/home", f!(crate::v1::get_home));
bp
}
/// All user-related routes and constructors.
fn user_bp() -> Blueprint {
let mut bp = Blueprint::new();
bp.constructor(f!(crate::user::get_session), Lifecycle::RequestScoped);
bp.route(GET, "/user", f!(crate::user::get_user));
bp
}
This example registers two routes:
GET /home
GET /user
It also registers two constructors:
crate::user::get_session
, forSession
;crate::db_connection_pool
, forConnectionPool
.
Since we are nesting the user_bp
blueprint, the get_session
constructor will only
be available to the routes declared in the user_bp
blueprint.
If a route declared in home_bp
tries to inject a Session
, Pavex will report an error
at compile-time, complaining that there is no registered constructor for Session
.
In other words, all constructors declared against the user_bp
blueprint are private
and isolated from the rest of the application.
The db_connection_pool
constructor, instead, is declared against the parent blueprint
and will therefore be available to all routes declared in home_bp
and user_bp
—i.e.
nested blueprints inherit all the constructors declared against their parent(s).
§Precedence
If a constructor is declared against both the parent and one of its nested blueprints, the one declared against the nested blueprint takes precedence.
use pavex::f;
use pavex::blueprint::{Blueprint, router::GET};
use pavex::blueprint::constructor::Lifecycle;
fn app() -> Blueprint {
let mut bp = Blueprint::new();
// This constructor is registered against the root blueprint and it's visible
// to all nested blueprints.
bp.constructor(f!(crate::global::get_session), Lifecycle::RequestScoped);
bp.nest(user_bp());
// [..]
bp
}
fn user_bp() -> Blueprint {
let mut bp = Blueprint::new();
// It can be overridden by a constructor for the same type registered
// against a nested blueprint.
// All routes in `user_bp` will use `user::get_session` instead of `global::get_session`.
bp.constructor(f!(crate::user::get_session), Lifecycle::RequestScoped);
// [...]
bp
}
§Singletons
There is one exception to the precedence rule: constructors for singletons (i.e.
using Lifecycle::Singleton
).
Pavex guarantees that there will be only one instance of a singleton type for the entire
lifecycle of the application. What should happen if two different constructors are registered for
the same Singleton
type by two nested blueprints that share the same parent?
We can’t honor both constructors without ending up with two different instances of the same
type, which would violate the singleton contract.
It goes one step further! Even if those two constructors are identical, what is the expected behaviour? Does the user expect the same singleton instance to be injected in both blueprints? Or does the user expect two different singleton instances to be injected in each nested blueprint?
To avoid this ambiguity, Pavex takes a conservative approach: a singleton constructor
must be registered exactly once for each type.
If multiple nested blueprints need access to the singleton, the constructor must be
registered against a common parent blueprint—the root blueprint, if necessary.
sourcepub fn nest(&mut self, blueprint: Blueprint)
pub fn nest(&mut self, blueprint: Blueprint)
Nest a Blueprint
under the current Blueprint
(the parent), without adding a common prefix to all the new routes.
Check out Blueprint::nest_at
for more details.
sourcepub fn fallback(&mut self, callable: RawIdentifiers) -> RegisteredFallback<'_>
pub fn fallback(&mut self, callable: RawIdentifiers) -> RegisteredFallback<'_>
Register a fallback handler to be invoked when an incoming request does not match
any of the routes you registered with Blueprint::route
.
If you don’t register a fallback handler, the default framework fallback will be used instead.
If a fallback handler has already been registered against this Blueprint
,
it will be overwritten.
§Example
use pavex::{f, blueprint::{Blueprint, router::GET}};
use pavex::response::Response;
fn handler() -> Response {
// [...]
}
fn fallback_handler() -> Response {
// [...]
}
let mut bp = Blueprint::new();
bp.route(GET, "/path", f!(crate::handler));
// The fallback handler will be invoked for all the requests that don't match `/path`.
// E.g. `GET /home`, `POST /home`, `GET /home/123`, etc.
bp.fallback(f!(crate::fallback_handler));
§Signature
A fallback handler is a function (or a method) that returns a Response
, either directly
(if infallible) or wrapped in a Result
(if fallible).
Fallback handlers can take advantage of dependency injection, like any
other component.
You list what you want to see injected as function parameters
and Pavex will inject them for you in the generated code.
§Nesting
You can register a single fallback handler for each blueprint. If your application takes advantage of nesting, you can register a fallback against each nested blueprint in your application as well as one for the top-level blueprint.
Let’s explore how nesting affects the invocation of fallback handlers.
§Nesting without prefix
The fallback registered against a blueprint will be invoked for all the requests that match the path of a route that was directly registered against that blueprint, but don’t satisfy their method guards.
use pavex::{f, blueprint::{Blueprint, router::GET}};
use pavex::response::Response;
fn fallback_handler() -> Response {
// [...]
}
let mut bp = Blueprint::new();
bp.route(GET, "/home", f!(crate::home_handler));
bp.nest({
let mut bp = Blueprint::new();
bp.route(GET, "/route", f!(crate::route_handler));
bp.fallback(f!(crate::fallback_handler));
bp
});
In the example above, crate::fallback_handler
will be invoked for incoming POST /route
requests: the path matches the path of a route registered against the nested blueprint
(GET /route
), but the method guard doesn’t (POST
vs GET
).
If the incoming requests don’t have /route
as their path instead (e.g. GET /street
or GET /route/123
), they will be handled by the fallback registered against the parent
blueprint—the top-level one in this case.
Since no fallback has been explicitly registered against the top-level blueprint, the
default framework fallback will be used instead.
§Nesting with prefix
If the nested blueprint includes a nesting prefix (e.g. bp.nest_at("/api", api_bp)
),
its fallback will also be invoked for all the requests that start with the prefix
but don’t match any of the route paths registered against the nested blueprint.
use pavex::{f, blueprint::{Blueprint, router::GET}};
use pavex::response::Response;
fn fallback_handler() -> Response {
// [...]
}
let mut bp = Blueprint::new();
bp.route(GET, "/home", f!(crate::home_handler));
bp.nest_at("/route", {
let mut bp = Blueprint::new();
bp.route(GET, "/", f!(crate::route_handler));
bp.fallback(f!(crate::fallback_handler));
bp
});
In the example above, crate::fallback_handler
will be invoked for both POST /route
and POST /route/123
requests: the path of the latter doesn’t match the path of the only
route registered against the nested blueprint (GET /route
), but it starts with the
prefix of the nested blueprint (/route
).
sourcepub fn error_observer(
&mut self,
callable: RawIdentifiers,
) -> RegisteredErrorObserver<'_>
pub fn error_observer( &mut self, callable: RawIdentifiers, ) -> RegisteredErrorObserver<'_>
Register an error observer to intercept and report errors that occur during request handling.
§Guide
Check out the “Error observers” section of Pavex’s guide for a thorough introduction to error observers in Pavex applications.
§Example
use pavex::f;
use pavex::blueprint::Blueprint;
pub fn error_logger(e: &pavex::Error) {
tracing::error!(
error.msg = %e,
error.details = ?e,
"An error occurred while handling a request"
);
}
let mut bp = Blueprint::new();
bp.error_observer(f!(crate::error_logger));