Controllers
Two controller patterns coexist. Choose whichever fits the call site — they interoperate.
Stateless (unit struct with associated functions)
The simplest shape, and the easiest to read. Methods are plain
async fn(Request) -> Response.
use oxide_http::{Request, Response, StatusCode};
pub struct StatsController;
impl StatsController { pub async fn index(req: Request) -> Response { // ... Response::text("ok") }
pub async fn show(req: Request) -> Response { let Some(id) = req.param("id").and_then(|s| s.parse::<i64>().ok()) else { return Response::with_status(StatusCode::BAD_REQUEST); }; // ... Response::text(format!("showing {id}")) }}Route file:
app.get("/stats", StatsController::index);app.get("/stats/{id}", StatsController::show);Typed extractor handlers
A handler can take a validated #[request]-annotated struct directly.
Validation failure auto-returns a 422 envelope; the method body only
runs on a valid payload.
use oxide_http::request;
#[request]pub struct RegisterRequest { #[validate(required, email)] pub email: Option<String>, #[validate(required, length(min = 8))] pub password: Option<String>,}
pub struct AuthController;
impl AuthController { pub async fn register(form: RegisterRequest) -> Response { let email = form.email.expect("validated as Some"); // ... Response::text(format!("registered {email}")) }}Injectable (#[api_resource])
When your controller holds services (e.g. Database), derive #[injectable]
on the struct and #[api_resource] on the impl. The IoC container
instantiates it from bindings registered in service providers.
use std::sync::Arc;use oxide_db::Database;use oxide_http::prelude::*;
#[injectable]pub struct TodoController { db: Arc<Database>,}
#[api_resource]impl TodoController { async fn index(&self, query: IndexRequest) -> Response { // `self.db` is available; `query` validated from the request. // ... Response::text("ok") }
async fn show(&self, id: i64) -> Response { // Path param `id` auto-extracted + parsed via FromStr. // ... Response::text(format!("{id}")) }}Mount in a route file:
app.api_resource::<TodoController>("/todos");// registers: GET/POST /todos, GET/PUT/DELETE /todos/{id}Which to pick
- Stateless when the controller needs no per-request services — fastest to read, no macros.
- Typed extractor when the handler needs automatic form validation.
- Injectable with
#[api_resource]for constructor-injected services and standard REST resource mapping.