Routing
A Router is a list of routes checked in order: the first one whose method and path match answers the request. Handlers are plain functions from &Request to Response.
Declaring routes#
use rustclamp::web::{Response, Router, error, json, redirect};
pub fn routes() -> Router {
Router::new()
.view("/", "welcome") // GET / serves the built view welcome.html
.get("/blog/{slug}", show_post) // {slug} matches one path segment
.get("/search", search)
.post("/subscribe", |request| {
let email = request.form("email").unwrap_or_default();
if email.contains('@') { redirect("/thanks") } else { error(422) }
})
.route("DELETE", "/api/posts/{id}", |request| {
Response::text(200, &format!("deleted {}", request.param("id").unwrap_or("?")))
})
}
| Method | Answers |
|---|---|
get(path, handler) | GET path |
post(path, handler) | POST path |
route(method, path, handler) | Any method: PUT, PATCH, DELETE… |
view(path, name) | GET path with the built page name |
Paths match exactly, segment by segment. A {name} segment matches any one non-empty segment and is read with request.param("name"); the value is not URL-decoded. A GET that matches no route falls through to a file in public/, and anything else is 404.
Controllers#
Rust has no base class, so a controller is a function in app/http/controllers/ that starts with use super::controller::*;, the shared imports:
use super::controller::*;
/// `GET /blog/{slug}`
pub fn show_post(request: &Request) -> Response {
match request.param("slug") {
Some("hello-world") => render("post", &[("title", &"Hello, world")]),
_ => error(404),
}
}
A handler that can fail returns web::Result and uses ?, as Laravel lets an exception bubble up. The error is logged with the method and path, and the visitor gets a 500:
pub fn index(request: &Request) -> web::Result {
let posts = Post::published(request.db())?;
Ok(render("blog", &[("posts", &posts)]))
}
router.up("/up") adds a health check like Laravel's: {"status":"ok"}, or 503 when the app's database does not answer.
Reading the request#
| Call | Returns |
|---|---|
request.method, request.path | The method and path, without the query string |
request.param("slug") | A {slug} route segment, not decoded |
request.query("q") | A decoded query-string value |
request.form("email") | A decoded field of a URL-encoded form body |
request.header("accept") | A header, matched case-insensitively |
request.cookie("theme") | A cookie value |
request.session() | The visitor's session, behind Sessions |
request.flash("Saved.") | A one-time message for the next page, shown as flash |
Building responses#
json(r#"{"status":"ok"}"#) // 200, application/json, a pre-serialized body
redirect("/thanks") // 302 with Location
view("welcome") // a built page
render("post", &[("title", &title)]) // a built view with data
error(404) // your error page, or the built-in one
Response::text(201, "created") // plain text with any status
Response::new(200, "text/csv", bytes) // any content type
.with_header("Content-Disposition", "attachment; filename=report.csv")
with_header turns line breaks into spaces, so a value taken from the request cannot inject headers of its own. See Views and assets for pages and error views.