Path parameters
A path parameter is a piece of the URL that varies: the 42 in /users/42.
You name it in the route's pattern, the router
captures whatever the request has in that position, and the handler reads it
by name.
The howdy/param module does the reading. Like howdy/query, each function
hands your handler a typed value and answers 400 on your behalf when the
value has the wrong shape.
Declaring parameters
A segment that starts with : captures one segment of the path. A final
segment that starts with * captures everything that is left:
import howdy/controller
pub fn user_controller() {
controller.new("users")
|> controller.get("/:id", show_user)
|> controller.get("/:id/orders/:order_id", show_order)
|> controller.get("/:id/files/*path", show_file)
}
A pattern can have as many :name segments as it needs, each with its own
name. It can have one *name, and only at the end.
Reading parameters
There is a function for each type, and both are designed for use:
import howdy/controller.{type Context}
import howdy/param
fn show_order(ctx: Context) {
use id <- param.int(ctx, "id")
use order_id <- param.int(ctx, "order_id")
// Both are Ints here.
order_service.find(id, order_id)
|> service.respond(ctx, order_to_json)
}
-
param.stringgives the captured text as it is. -
param.intparses it as a whole number.
When the value does not parse, the handler stops and the client gets a 400
with a JSON body. A request for /users/abc answers:
{"error":"parameter id must be an integer"}
Unlike a query string, a path parameter cannot be absent or repeated: if the
segment were missing, the route would not have matched. So there are no
optional or default forms. The one way to see
{"error":"missing parameter id"} is to ask for a name that is not in the
route's pattern, which means the handler and the pattern disagree. Treat it
as a bug, not as bad input.
Wildcards
A *name parameter is read the same way. It holds the rest of the path,
joined with /:
fn show_file(ctx: Context) {
use path <- param.string(ctx, "path")
controller.text(ctx, "You asked for " <> path)
}
A request for /users/7/files/reports/june.pdf gives reports/june.pdf. A
wildcard also matches when nothing is left, so /users/7/files reaches the
same handler with path set to "".
Be careful with a wildcard that names a file. Never join it to a directory
and read the result: a path such as ../../etc/passwd would walk out of the
directory. To serve files, use howdy/static, which checks for this.
What a value contains
A parameter is the segment exactly as it appeared in the URL. It is not
percent-decoded, so a request for /users/ada%20lovelace gives
"ada%20lovelace", not "ada lovelace". When a parameter may hold more
than letters, digits and hyphens, decode it yourself:
import gleam/uri
fn show_tag(ctx: Context) {
use raw <- param.string(ctx, "tag")
case uri.percent_decode(raw) {
Ok(tag) -> controller.text(ctx, tag)
Error(Nil) -> controller.status(ctx, 400)
}
}
A parameter never contains a /, because the path is split on / before
matching. Only a wildcard can span several segments.
Without a continuation
param.string and param.int answer the request themselves, so they only
work where a response can be returned. Anywhere else, such as inside a
guard, use controller.param. It returns a
plain Result:
fn owns_account(ctx: GuardedContext(User)) -> service.Result(Nil) {
case controller.param(ctx, "id") {
Ok(id) if id == ctx.guard.account_id -> Ok(Nil)
_ -> Error(service.Forbidden)
}
}
Parameter or query string?
Use a path parameter for what the URL identifies: which user, which order.
Use the query string for how to show it:
the page number, the sort order, a filter. A good test is whether the value
can be left out. If /users still makes sense without it, it belongs in the
query string.
Testing parameters
Put the value in the path, as a client would:
import howdy/testing
pub fn user_id_must_be_a_number_test() {
let res = testing.get("/users/abc") |> testing.send(app())
assert res.status == 400
assert testing.error(res) == Ok("parameter id must be an integer")
}
