Controllers and routes
Define HTTP methods, path parameters, and route matching behavior.
A controller groups related endpoints under a common base path. Lily discovers controllers at link time and creates them during application setup.
Controller contract#
Use #[derive(Controller)], a single #[base_path], ControllerTrait::new, and #[controller] impl together. Actions must be async and receive &self. Put helper methods in a separate, ordinary impl block.
Parameterized routes and creation#
use std::sync::Arc;
use lilyrs::http_api::{
controller, Controller, ControllerInitError, ControllerTrait, Created,
Extensions, HttpApiError, Json, Path, Query,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct UserPath { id: u64 }
#[derive(Deserialize)]
struct UserQuery { language: Option<String> }
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct CreateUser { name: String }
#[derive(Serialize)]
struct UserView { id: u64, name: String }
#[derive(Controller)]
#[base_path("/users")]
struct UsersController;
#[lilyrs::http_api::async_trait::async_trait]
impl ControllerTrait for UsersController {
async fn new(_: Arc<Extensions>) -> Result<Self, ControllerInitError> { Ok(Self) }
}
#[controller]
impl UsersController {
#[get("/:id")]
async fn read(&self, Path(path): Path<UserPath>, Query(query): Query<UserQuery>)
-> Result<UserView, HttpApiError>
{
Ok(UserView { id: path.id, name: query.language.unwrap_or_else(|| "tr".into()) })
}
#[post("/")]
async fn create(&self, Json(input): Json<CreateUser>)
-> Result<Created<UserView>, HttpApiError>
{
if input.name.trim().is_empty() {
return Err(HttpApiError::ValidationError("name is required".into()));
}
// Teaching example: in a real application, the domain service produces this result.
Ok(Created::new("/users/42", UserView { id: 42, name: input.name }))
}
}GET /users/42?language=tr separates parameters into typed DTOs. A successful POST /users returns 201 and Location: /users/42. This example does not persist a record; it demonstrates the HTTP contract. Move actual persistence into a domain service received through DI.
When the controller base path is /users and the action path is /, the combined route is /users; a trailing slash is not added automatically. Send requests to the registered path.
Route syntax#
| Path | Matches |
|---|---|
/users/me |
A static path |
/users/:id |
A single-segment parameter |
/files/*path |
The remaining trailing path |
A catch-all must be the last segment. Static routes take precedence over parameterized routes. Duplicate method/path pairs or ambiguous parameter paths with equal precedence cause a build error.
get, post, put, patch, delete, head, and options are built-in attributes. For a custom method, use #[route(method = "CUSTOM", path = "/...")]. With OpenAPI enabled, exclude a nonstandard method using #[openapi(skip)].
HEAD, 404, and 405#
If there is no explicit HEAD route, routing falls back to GET; the transport does not send the response body. A matching path with no matching method produces 405 and an Allow header. A path that does not match produces 404.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.