Skip to content

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.