Skip to main content

Module recipes

Module recipes 

Source
Expand description

§Recipes

Patterns and ideas for common use cases.

§Middleware with api()

An api handler can act as middleware by returning a handler that either halts the conn (blocking downstream handlers) or does nothing (letting them proceed).

The key trick: extract with Option<T> (which always succeeds), then decide whether to halt.

use trillium_api::{api, FromConn, Halt};
use trillium::{Conn, Handler, Status};

#[derive(Debug, Clone)]
struct User(String);

impl FromConn for User {
    async fn from_conn(conn: &mut Conn) -> Option<Self> {
        conn.request_headers()
            .get_str("x-user")
            .map(|s| User(s.to_owned()))
    }
}

async fn require_user(
    _conn: &mut Conn,
    user: Option<User>,
) -> Option<(Status, Halt)> {
    if user.is_none() {
        Some((Status::Forbidden, Halt))
    } else {
        None   // no-op — next handler runs
    }
}

// Place before your router in the handler tuple:
let app = TestServer::new((
    api(require_user),
    "hello, authenticated user",
)).await;

§Type aliases for complex extractors

When tuple extractors get long, a type alias keeps handler signatures readable:

type CreateArgs = (State<Arc<Db>>, Body<NewItem>, State<AppConfig>);

async fn create(
    conn: &mut Conn,
    (State(db), Body(input), State(config)): CreateArgs,
) -> Result<(Status, Json<Item>), AppError> {
    // ...
}

§Arc<T> for shared state

When your shared state is expensive to clone, wrap it in Arc. The trillium State<T> handler clones T into each conn, so using Arc<T> means only the pointer is cloned:

use std::sync::Arc;
use trillium_api::State;

struct Db { /* connection pool, etc. */ }

// In app setup:
let app = (
    State(Arc::new(Db { /* ... */ })),
    router,
);

// In handlers:
async fn list(_conn: &mut Conn, State(db): State<Arc<Db>>) -> Json<Vec<Item>> {
    // db is Arc<Db> — cheap to clone, shared across requests
}

§FromConn for shared state (borrow, don’t take)

State<T> calls take_state, which removes the value. If you need the state to remain available for other handlers or extractors, implement FromConn with conn.state().cloned() instead:

impl FromConn for Db {
    async fn from_conn(conn: &mut Conn) -> Option<Self> {
        conn.state().cloned() // borrows, doesn't remove
    }
}

This is especially important for state that’s used in multiple extractors within the same request (e.g., a database handle used by both a User extractor and the route handler itself).

§Returning (Status, Json<T>) for create endpoints

REST APIs commonly return 201 Created with a body. Since Status doesn’t halt, and Json<T> does, they compose naturally:

async fn create(
    _conn: &mut Conn,
    (db, Body(input)): (Db, Body<NewItem>),
) -> Result<(Status, Json<Item>), AppError> {
    let item = db.insert(input).await?;
    Ok((Status::Created, Json(item)))
}

§Domain objects as extractors

Rather than parsing route parameters in every handler, implement TryFromConn on your domain type to load it once from the route param + database:

impl TryFromConn for Todo {
    type Error = Status;

    async fn try_from_conn(conn: &mut Conn) -> Result<Self, Status> {
        let db = Db::from_conn(conn).await.ok_or(Status::InternalServerError)?;
        let id: u64 = conn.param("todo_id")
            .and_then(|p| p.parse().ok())
            .ok_or(Status::BadRequest)?;
        db.find_todo(id).await.ok_or(Status::NotFound)
    }
}

// Now handlers receive a loaded Todo directly:
async fn show(_conn: &mut Conn, todo: Todo) -> Json<Todo> {
    Json(todo)
}

async fn update(
    _conn: &mut Conn,
    (todo, Body(input)): (Todo, Body<UpdateTodo>),
) -> Result<Json<Todo>, AppError> {
    // ...
}

§Query string extraction

There’s no built-in query string extractor, but TryFromConn makes it straightforward:

#[derive(Deserialize)]
struct Pagination {
    page: Option<u64>,
    per_page: Option<u64>,
}

impl TryFromConn for Pagination {
    type Error = Status;

    async fn try_from_conn(conn: &mut Conn) -> Result<Self, Status> {
        serde_urlencoded::from_str(conn.querystring())
            .map_err(|_| Status::BadRequest)
    }
}

§cancel_on_disconnect for expensive operations

cancel_on_disconnect is like api, but cancels the handler future if the client disconnects. The handler function does not receive &mut Conn — all request data must come through extractors:

use trillium_api::cancel_on_disconnect;

async fn expensive_report(
    (db, pagination): (Db, Pagination),
) -> Result<Json<Report>, AppError> {
    // If the client hangs up, this future is dropped
    db.generate_report(pagination).await
        .map(Json)
        .map_err(AppError::from)
}

router().get("/report", cancel_on_disconnect(expensive_report))