Skip to main content
Version: 1.0

API Layer

rustdocs

The trillium-api crate provides an extractor-based handler interface for building typed APIs. Instead of reading from Conn by hand, you write async functions that declare what data they need and the framework extracts it automatically.

Basic usage

Wrap an async function with api() to turn it into a Handler:

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

async fn hello(_conn: &mut Conn, _: ()) -> Json<&'static str> {
Json("hello, world")
}

fn main() {
trillium_smol::run(api(hello));
}

The function signature drives behavior:

  • The first parameter is always &mut Conn.
  • The second parameter is extracted from the request using TryFromConn or FromConn.
  • The return value is run as a Handler on the conn — return a status, a Json<T>, a string, or anything else that implements Handler.

Extractors

TypeExtractsFails if
()NothingNever
Json<T>Deserializes JSON request body into TBody is not valid JSON, or T fails to deserialize
Body<T>Deserializes body based on Content-TypeUnsupported content type, or deserialization fails
State<T>Takes T from conn stateState is absent (halts with no body)
StringRequest body as a stringBody is not valid UTF-8
Vec<u8>Request body as raw bytesNever
MethodThe HTTP methodNever
HeadersClone of request headersNever
(A, B, ...)Multiple extractors as a tupleIf any constituent fails

JSON request and response

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

#[derive(Deserialize)]
struct CreatePost { title: String, body: String }

#[derive(Serialize)]
struct Post { id: u64, title: String, body: String }

async fn create_post(_conn: &mut Conn, Json(input): Json<CreatePost>) -> Json<Post> {
// In a real app you'd persist this somewhere
Json(Post { id: 1, title: input.title, body: input.body })
}

Error handling

Errors in trillium-api are handlers. When an extraction fails, the extractor's error type is run on the conn instead of your function. When a handler returns Result<T, E> and the result is Err(e), e is run as a handler.

Extraction errors

Json<T> and Body<T> use trillium_api::Error as their error type, which responds with a structured JSON error body and an appropriate status:

  • Parse errors → 422 Unprocessable Entity
  • Missing content type → 415 Unsupported Media Type
  • I/O errors → 400 Bad Request

These are handled automatically — if a client sends malformed JSON to a handler that extracts Json<T>, the response is a structured error without your function being called.

Result return types

Your handler can return Result<T, E> where both T and E implement Handler. The idiomatic pattern is to define an error type for your application:

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

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

impl Handler for ApiError {
async fn run(&self, conn: Conn) -> Conn {
conn.with_json(self)
.with_status(Status::BadRequest)
.halt()
}
}

async fn divide(_conn: &mut Conn, Json((a, b)): Json<(f64, f64)>) -> Result<Json<f64>, ApiError> {
if b == 0.0 {
Err(ApiError { message: "division by zero".into() })
} else {
Ok(Json(a / b))
}
}

For extraction errors specifically, Status alone is a valid and simple error type — it sets the status code when run as a handler:

use trillium::{Conn, Status};
use trillium_api::TryFromConn;
use trillium_router::RouterConnExt;

struct UserId(u64);

impl TryFromConn for UserId {
type Error = Status;

async fn try_from_conn(conn: &mut Conn) -> Result<Self, Status> {
conn.param("user_id")
.and_then(|p| p.parse().ok())
.map(UserId)
.ok_or(Status::BadRequest)
}
}

Combining with the router

api() returns a Handler, so it composes naturally with the router:

use trillium_router::router;
use trillium_api::api;

let app = router()
.get("/posts", api(list_posts))
.post("/posts", api(create_post))
.get("/posts/:id", api(get_post));

JSON serialization backend

trillium-api does not enable any default features, but you likely want to select either serde_json or sonic-rs to get the most out of this crate. The two features are mutually exclusive.

See the rustdocs for the full extractor API, custom extractor implementation, and return type details.