Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Handler

The Quokka handlers provide you with interfaces to load and process data, it also comes with some input and output helpers for JSON and templating/HTML representation of this data.

DataLoader

The general idea is to have a DataLoader which loads data from a database or other sources and refines that data for displaying it. Gathering of the data might be outsourced to some services to keep code clean as well as formatting it properly or merging it with other data. It is most sensical to use it in GET routes as it cannot interact with the request body.

*Renderer

To use (or render) the data created by the DataLoader you can use the builtin TemplateRenderer or the JsonRenderer.

Example

Note: The provided get_router method is an excerp of a bundle/Pouch implementation.

#[derive(serde::Serialize)]
struct PersonData {
    name: String,
    age: u8,
}

#[derive(Clone, FromState)]
struct PersonDataLoader {}

impl<S: Send + Sync + Clone + 'static> DataLoader<S> for PersonDataLoader {
    // The Loader still can get data from the request using axum extractors.
    // But it has no access to the body or any extractor that would require it.
    type Args = Path<String>;

    // The dete does not require you to choose the representation later. It only has to implement `serde::Serialize`.
    type Data = PersonData;

    // The error type can be any. In this case our handler cannot fail so we take the `Infallible`.
    // It only has to implement the `std::error::Error` trait which already allows us to `Display` it.
    //
    // In case of an error it will be attached as an extension to a generic server error which can be intercepted using a middleware.
    type Error = Infallible;

    // This part loads your data. As the `PersonDataLoader` implements the `FromState` and is built from the state when a requests enters
    // the application it can make use of any kind of service or substate directly.
    async fn load_data(&self, name: Self::Args) -> Result<Self::Data, Self::Error> {
        Ok(PersonData {
            name: name.to_string(),
            age: 42,
        })
    }
}

fn get_router(&self) -> axum::Router<S> {
    axum::Router::new()
        .route(
            "/api/v1/person/{name}",
            JsonRenderer::<PersonDataLoader>::new().into(),
        )
}

DataHandler

The DataHandler is supposed to handle the request body for a given request. The provided handlers ForwHandler and JsonHandler expect you to provide a separate renderer (See the renderer section of the DataLoaders).

Example

Note: The provided get_router method is an excerp of a bundle/Pouch implementation.

use std::convert::Infallible;

use axum::{extract::Path, routing::post};
use quokka::{state::*, handler::{DataHandler, JsonHandler, JsonRenderer}};

#[derive(Clone, FromState)]
struct UpdatePersonDataHandler {}

#[derive(serde::Deserialize)]
struct UpdatePersonData {
    age: u8,
}

impl<S: Send + Sync + Clone + 'static> DataHandler<S> for UpdatePersonDataHandler {
    // Also here you can only use extractors that do not require the body as the body is being extracted by the handler as whatever input
    // format it requires
    type Args = Path<String>;

    // The handler will parse the request data (eg. JSON) and Deserializes it into this data type, thus it needs to implement the
    // `serde::Deserialize` trait
    type Body = UpdatePersonData;

    // Also this error type has to implement the `std::error::Error` trait, but instead of being returned directly to the client, the
    // request will be forwarded to the `DataLoader` which then can extract the error as an extension as type `HandlerError<Error>` (where
    // `Error` is this here specified error type).
    type Error = Infallible;

    // The `DataHandler` is only supposed to do action on the request body, so it's path has to match with the corresponding `DataLoader`
    // which loads the data.
    //
    // As this types' name implies it, the here provided data can be extracted with an extension on the `DataLoader` but it is usually
    // expected to construct the data only with the required info (through it's `Args` type) to ensure consistent view of this data.
    type Extension = ();

    async fn process_data(
        &self,
        path: Self::Args,
        body: Self::Body,
    ) -> Result<Self::Extension, Self::Error> {
        // self.service.update_user(path.0, body.age).await; // Update the user here

        Ok(())
    }
}

fn get_router() -> axum::Router<S> {
    axum::Router::new()
        .route(
            "/api/v1/person/{name}",
            post(JsonHandler::<UpdatePersonDataHandler, _>::new(JsonRenderer::<PersonalDataLoader>::new())),
        )
}

This example shows the optimal use of a DataHandler and a DataLoader. For cases like a CreatePersonDataHandler, which would get it's full data from the request and then gets persistet, thus created with a new path, this has to be different as a listing (the default action for a route without identifier) does not make sense for a response when a new entity is created. For this usecase the implementation would look like the following

use std::convert::Infallible;

use axum::Extension;
use quokka::{state::*, handler::{DataHandler, DataLoader}};

#[derive(Clone, FromState)]
struct CreatePersonDataHandler {}

#[derive(Clone)]
struct PersonIdentifier(i32);

#[derive(serde::Deserialize)]
struct CreatePersonData {
    name: String,
    age: u8,
}

#[derive(Debug, thiserror::Error)]
enum CreatePersonError {
    #[error("You are too old: {0}")]
    TooOld(u8),
    #[error("You are too young: {0}")]
    TooYoung(u8),
}

impl<S: Send + Sync + Clone + 'static> DataHandler<S> for CreatePersonDataHandler {
    // Wo don't need args as we handle the full body
    type Args = ();

    // The handler will parse the request data (eg. JSON) and Deserializes it into this data type, thus it needs to implement the
    // `serde::Deserialize` trait
    type Body = CreatePersonData;

    // Also this error type has to implement the `std::error::Error` trait, but instead of being returned directly to the client, the
    // request will be forwarded to the `DataLoader` which then can extract the error as an extension as type `HandlerError<Error>` (where
    // `Error` is this here specified error type).
    type Error = CreatePersonError;

    // Now we do want to attach the created Person's identifier so we can load it later
    type Extension = PersonIdentifier;

    async fn process_data(
        &self,
        _: Self::Args,
        body: Self::Body,
    ) -> Result<Self::Extension, Self::Error> {
        if body.age > 99 {
            return Err(CreatePersonError::TooOld(body.age));
        }

        if body.age > 9 {
            return Err(CreatePersonError::TooYoung(body.age));
        }

        // let id = self.service.create_person(body.name, body.age).await; // Update the user here

        todo!() // this should return `id` - if we would have one
    }
}

impl<S: Send + Sync + Clone + 'static> DataLoader<S> for CreatePersonDataHandler {
    type Args = (Option<Extension<PersonIdentifier>>, Option<Extension<CreatePersonError>>);

    // The usual Data type as described above
    type Data = PersonData;

    // This handler does nothing actually, so we don't need to specify an error
    type Error = JsonError;

    async fn load_data(
        &self,
        (identifier, error): Self::Args,
    ) -> Result<Self::Data, Self::Error> {
        if let Some(Extension(error)) = error {
            // We convert our error here because we use the same middleware for all of our JSON APIs which only outputs the current error
            // return Err(JsonError(format!("{error}")));
        }

        if let Some(Extension(identifier)) = identifier {
            // return Ok(self.service.load_person(identifier));
        }

        todo!()
    }
}

Here we use the same struct for loading and handling our data, as this whole action is not very reusable anyway.