Skip to main content

Module return_types

Module return_types 

Source
Expand description

§Return types — handlers all the way down

The return type of an api handler is not a response body or a status code — it’s a Handler. The returned handler is then run on the conn, giving you the full power of trillium’s handler model in your return value.

This is the key insight of trillium-api: rather than inventing a new response type, it reuses the composable Handler trait that you already know from the rest of trillium.

§The simplest returns

Several common types already implement Handler:

use trillium_api::api;
use trillium::{Conn, Status};

/// `()` is the no-op handler — doesn't set status, body, or halt.
/// Useful when you've already modified the conn via `&mut Conn`.
async fn modify_conn(conn: &mut Conn, _: ()) {
    conn.set_status(200);
    conn.set_body("done");
}

/// `&'static str` halts the conn with 200 + that string as the body.
async fn string_body(_conn: &mut Conn, _: ()) -> &'static str {
    "hello"
}

/// `Status` sets the status code (but does not halt or set a body).
async fn no_content(_conn: &mut Conn, _: ()) -> Status {
    Status::NoContent
}

§JSON responses

Json<T> and Body<T> implement Handler for T: Serialize, serializing the value and setting the appropriate content type.

use trillium_api::{api, Body, Json};
use trillium::Conn;
use serde::Serialize;

#[derive(Serialize)]
struct User { name: String }

/// Json always serializes as application/json
async fn as_json(_conn: &mut Conn, _: ()) -> Json<User> {
    Json(User { name: "alice".into() })
}

/// Body negotiates the content type based on the Accept header
async fn as_body(_conn: &mut Conn, _: ()) -> Body<User> {
    Body(User { name: "alice".into() })
}

§Tuples of handlers

Handler tuples run left to right, stopping at the first handler that halts. This lets you compose multiple response properties:

use trillium_api::{api, Json};
use trillium::{Conn, Status};
use serde::Serialize;

#[derive(Serialize)]
struct Item { id: u64 }

/// Sets status to 201, then serializes the JSON body (which halts).
async fn create(_conn: &mut Conn, _: ()) -> (Status, Json<Item>) {
    (Status::Created, Json(Item { id: 42 }))
}

You can also include Headers in the tuple to set response headers, or any other Handler.

§Option<H> — conditional responses

Option<impl Handler> runs the inner handler if Some, or does nothing if None (no-op, doesn’t halt):

use trillium_api::{api, Json};
use trillium::{Conn, Status};

async fn maybe(_conn: &mut Conn, _: ()) -> Option<Json<&'static str>> {
    if true { Some(Json("found")) } else { None }
}

§Result<T, E> — fallible responses

When both T and E implement Handler, Result<T, E> is also a handler — running T on Ok or E on Err:

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

#[derive(serde::Serialize)]
struct ErrorBody { message: String }

async fn might_fail(_conn: &mut Conn, _: ()) -> Result<Json<&'static str>, (Status, Json<ErrorBody>)> {
    if true {
        Ok(Json("success"))
    } else {
        Err((Status::InternalServerError, Json(ErrorBody { message: "boom".into() })))
    }
}

For the common case of a custom error type, see error_handling.

§Using &mut Conn directly

The first parameter is always &mut Conn. You can use it to set response properties directly, and return () (or any handler) to finish:

use trillium_api::api;
use trillium::Conn;

async fn direct(conn: &mut Conn, _: ()) {
    conn.set_status(200);
    conn.insert_response_header("x-custom", "value");
    conn.set_body("done");
}

This is useful when you need to modify the conn in ways that don’t map cleanly to a return value — setting headers, caching state for later extractors, or conditionally modifying the response.

§Important: concrete return types

Return types must be concrete-> impl Handler does not work as a return type for api handler functions. This is because the type must be known at compile time for the ApiHandler struct’s type parameters. Use concrete types instead:

// Won't compile:
async fn bad(_conn: &mut Conn, _: ()) -> impl Handler { Json("hi") }

// Do this instead:
async fn good(_conn: &mut Conn, _: ()) -> Json<&'static str> { Json("hi") }

When you need to return different types from different branches, use Result, Option, or a custom enum that implements Handler.

§ApiHandler sets 200 automatically

If your returned handler sets a response body but no status code, api automatically sets 200 OK. You only need to set a status explicitly when you want something other than 200.