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

8. State and dependencies

Almost every API needs something shared across requests: a database pool, configuration, an HTTP client. And you often want to reuse extraction logic, such as “the authenticated user”. In FastAPI both go through Depends; in lesto, as in axum, they go through extractors.

Shared state: State<S>

Declare a type for the state; it must be Clone (it usually holds Arcs):

use std::sync::{Arc, Mutex};
use lesto::prelude::*;

#[derive(Clone, Default)]
struct AppState {
    counter: Arc<Mutex<u64>>,
}

/// Increment and return the counter.
#[lesto::post("/hits")]
async fn hit(State(state): State<AppState>) -> String {
    let mut n = state.counter.lock().unwrap();
    *n += 1;
    n.to_string()
}

#[lesto::main]
async fn main() -> std::io::Result<()> {
    App::new()
        .routes(routes![hit])
        .with_state(AppState::default())
        .serve()
        .await
}

State<AppState> in the handler says “I want the state”; .with_state(...) on the App provides it. The compiler checks that the two types match: forget with_state, or pass a different type, and the program does not compile. State does not appear in the documentation, because it does not come from the client.

In tests and larger projects an explicit type helps: App::<AppState>::new().

Sub-states with FromRef

If a handler only wants the pool and not the whole state, axum offers FromRef:

use lesto::axum::extract::FromRef;

#[derive(Clone)]
struct AppState {
    db: DbPool,
    config: Arc<Config>,
}

impl FromRef<AppState> for DbPool {
    fn from_ref(s: &AppState) -> DbPool { s.db.clone() }
}

async fn handler(State(db): State<DbPool>) { .. }     // works with with_state(AppState)

With axum’s #[derive(FromRef)] (macros feature) the impl is generated for every field.

Extension<T>

For values inserted by a middleware (a request id, a user authenticated by an external layer) there is Extension<T>, also invisible in the documentation:

async fn handler(Extension(req_id): Extension<RequestId>) { .. }

Custom extractors: lesto’s Depends

An extractor is a type that knows how to build itself from the request. Writing one means implementing FromRequestParts, and optionally OperationInput to tell the documentation what it requires.

The classic example: the current user derived from the Bearer token.

use lesto::axum::extract::FromRequestParts;
use lesto::http::request::Parts;
use lesto::prelude::*;
use lesto::{BearerAuth, OperationBuilder, OperationInput, Rejection};

#[lesto::model]
#[derive(Clone)]
pub struct User {
    pub id: u64,
    pub name: String,
}

/// The authenticated user. Use it as a handler argument.
pub struct CurrentUser(pub User);

impl<S: Send + Sync> FromRequestParts<S> for CurrentUser {
    type Rejection = Rejection;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Rejection> {
        // Reuse the Bearer extractor: missing header → 401 with WWW-Authenticate.
        let auth = Bearer::<BearerAuth>::from_request_parts(parts, state).await?;
        // This is where you would verify the token (a session, a key...). For an OpenID
        // Connect JWT, `lesto::oidc::Jwt` does it for you (chapter 9).
        match auth.token() {
            "secret" => Ok(CurrentUser(User { id: 1, name: "Ada".into() })),
            _ => Err(HttpError::unauthorized("invalid token").into()),
        }
    }
}

// The documentation inherits Bearer's: bearerAuth scheme + 401 response.
impl OperationInput for CurrentUser {
    fn describe(builder: &mut OperationBuilder<'_>) {
        Bearer::<BearerAuth>::describe(builder);
    }
}

/// Profile of the authenticated user.
#[lesto::get("/me")]
async fn me(CurrentUser(user): CurrentUser) -> Json<User> {
    Json(user)
}
curl http://127.0.0.1:8000/me -H 'Authorization: Bearer secret'
{"id":1,"name":"Ada"}

Every handler that takes CurrentUser as an argument gets an already verified user, and its documentation shows the lock icon. It plays the same role as user: User = Depends(get_current_user).

If the extractor has nothing to do with the client (it only reads state or extensions), you need no OperationInput at all: an extractor without one is accepted and simply left out of the document. Teams that want every argument documented turn on lesto’s strict-docs feature, which makes a missing impl a compile error; impl OperationInput for MyType {} with an empty body then says “nothing to document” explicitly.

Dependencies with parameters

In FastAPI a dependency can itself take query parameters. In lesto you compose extractors inside your own:

pub struct Page { pub offset: usize, pub limit: usize }

impl<S: Send + Sync> FromRequestParts<S> for Page {
    type Rejection = Rejection;
    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Rejection> {
        let Query(p) = Query::<Pagination>::from_request_parts(parts, state).await?;
        Ok(Page { offset: p.skip, limit: p.limit })
    }
}

impl OperationInput for Page {
    fn describe(builder: &mut OperationBuilder<'_>) {
        Query::<Pagination>::describe(builder);     // documents skip and limit
    }
}

Recap

  • State<S> + with_state(s): shared state, checked by the compiler.
  • Extension<T> for values inserted by middleware.
  • A custom extractor = FromRequestParts (+ OperationInput for the documentation). Extractors compose: Bearer, Query, State inside yours.

Next: Security.