Routing
- Creating a router
- Declaring routes
- Encoding and decoding parameters
- Building responses
- Content negotiation
- Raw routes
- Redirecting routes
Routing is the core of Presto. A RestRouter
maps incoming HTTP requests identified by method and path to handler
procedures, unmarshalling request parameters into Nim types along the way.
Creating a router
RestRouter.init
takes a pattern-validation callback. This callback is invoked for every path
segment that corresponds to a {pattern} in a route, letting you reject
malformed input before the handler runs. It returns 0 when the value is
acceptable and any non-zero value to reject the request.
proc validate(pattern: string, value: string): int =
case pattern
of "{id}":
# only accept numeric ids
if value.allCharsInSet({'0' .. '9'}): 0 else: 1
else:
1
If you don't need validation, provide a callback that always returns 0.
Declaring routes
Routes are declared with the api macro
using Nim's do notation. The handler's return type must be
RestApiResponse.
router.api(MethodGet, "/ping") do () -> RestApiResponse:
RestApiResponse.response("pong", Http200, "text/plain")
The handler's parameters describe what Presto should extract from the request. Presto inspects their names and types and generates the extraction and decoding code for you.
Path parameters
A segment written as {name} in the route path becomes a required parameter of
the same name. Path parameters are decoded into a Result[T, cstring], so you
can check them withisErr() / error() and read the value with get().
router.api(MethodGet, "/users/{id}") do (id: int) -> RestApiResponse:
if id.isErr():
return RestApiResponse.error(Http400, $id.error())
RestApiResponse.response("user " & $id.get())
Query parameters
Parameters that are not part of the path become query-string parameters. Use
Option[T] for an optional value and seq[T] to collect a repeated key.
router.api(MethodGet, "/search") do (
q: Option[string], tag: seq[string]) -> RestApiResponse:
# q is Option[Result[string]]: present? then decoded?
let query =
if q.isSome(): q.get().get() else: ""
# tag collects ?tag=a&tag=b&tag=c into @["a", "b", "c"]
RestApiResponse.response("searching " & query & " in " & $tag)
Optional query parameters have type Option[Result[T, cstring]]: the outer
Option tells you whether the key was present, and the inner Result tells you
whether decoding succeeded. A seq[T] parameter is instead a single
Result[seq[T], cstring], and is empty when the key is absent.
The request body
Add an argument of type Option[ContentBody]
to receive the raw request body (available for POST, PUT, PATCH and
DELETE). The name of the argument is up to you.
router.api(MethodPost, "/echo") do (
contentBody: Option[ContentBody]) -> RestApiResponse:
if contentBody.isNone():
return RestApiResponse.error(Http400, "body required")
let body = contentBody.get()
echo "content-type: ", body.contentType
RestApiResponse.response(string.fromBytes(body.data))
The response object
Add an argument of type HttpResponseRef to take over the response yourself, e.g. to stream a body with sendBody. You may still return a RestApiResponse; if you have already responded, Presto will not send anything further.
router.api(MethodGet, "/stream") do (
resp: HttpResponseRef) -> RestApiResponse:
await resp.sendBody("streamed")
RestApiResponse.response("") # ignored: already responded
Reserved keywords as parameter names
Parameter names coming from the URL may collide with Nim keywords. Quote them with backticks:
router.api(MethodGet, "/kw/{type}") do (`type`: string) -> RestApiResponse:
RestApiResponse.response(`type`.get())
A path may contain at most 64 segments. Requests with more segments are rejected
with 400 Bad Request.
Encoding and decoding parameters
Presto does not assume any particular serialization format. Instead, it calls user-supplied procedures to convert between wire representations and Nim values. You must provide these for every type you use as a parameter, body, or response.
| Procedure | Used by | Purpose |
|---|---|---|
decodeString(t: typedesc[T], value: string): RestResult[T] | server | decode a path/query parameter |
decodeBytes(t: typedesc[T], value: openArray[byte], contentType: Opt[ContentTypeData]): RestResult[T] | client | decode a response body |
encodeString(value: T): RestResult[string] | client | encode a path/query parameter |
encodeBytes(value: T, contentType: string): RestResult[seq[byte]] | client | encode a request body |
A minimal decodeString for int on the server side:
proc decodeString*(t: typedesc[int], value: string): RestResult[int] =
var v: int
if parseSaturatedNatural(value, v) == 0:
err("Unable to decode decimal string")
else:
ok(v)
By convention, seq[byte] values are encoded as hex strings (for example
0x7465737431 decodes to "test1"). This lets you pass binary data safely
through URLs.
Custom and generic types work the same way: you decide how they map to
strings and bytes. See tests/helpers.nim in the repository for a complete set
of encoders/decoders covering integers, strings, byte sequences, and custom
variant objects.
Building responses
RestApiResponse has three
constructors, each with several overloads.
Content responses
The response constructor sends a
body:
discard RestApiResponse.response("hello") # 200, text/plain
discard RestApiResponse.response("{}", Http201, "application/json")
discard RestApiResponse.response("body", Http200,
headers = [("X-Custom", "1")]) # extra headers
discard RestApiResponse.response(Http204) # no body
Error responses
The error constructor sends an error
status with an optional message:
discard RestApiResponse.error(Http404, "not found")
discard RestApiResponse.error(Http500, "boom", "text/plain",
headers = [("Retry-After", "5")])
Redirects
The redirect constructor issues an
HTTP redirect:
discard RestApiResponse.redirect(Http307, "/new/location")
discard RestApiResponse.redirect(Http307, "/new/location", preserveQuery = true)
When preserveQuery is true, the original request's query string is merged
into the redirect target.
When you pass headers and a contentType, the explicit contentType argument
wins over any Content-Type present in the headers table. The same applies to
the Location header for redirects.
Content negotiation
Inside a handler you can inspect the client's Accept header and pick a
supported media type with preferredContentType. It returns a Result; when
nothing matches, respond with 406 Not Acceptable.
const
typeJson = MediaType.init("application/json")
typeText = MediaType.init("text/plain")
router.api(MethodGet, "/negotiate") do () -> RestApiResponse:
let preferred = preferredContentType(typeJson, typeText)
if preferred.isErr():
return RestApiResponse.error(Http406, "")
if preferred.get() == typeJson:
RestApiResponse.response("{}", Http200, "application/json")
else:
RestApiResponse.response("ok", Http200, "text/plain")
Raw routes
Sometimes you want the request untouched: no body decoding, direct access to
HttpRequestRef. Use rawApi instead
of api. Raw handlers receive the request symbol in scope.
router.rawApi(MethodPost, "/raw") do () -> RestApiResponse:
let contentType = request.headers.getString("content-type")
let body = await request.getBody()
RestApiResponse.response("got " & $len(body) & " bytes of " & contentType)
Redirecting routes
You can register a redirect from one route pattern to another compatible one
with the redirect macro. Both
patterns must contain the same set of {names} (they may appear in a different
order).
router.redirect(MethodGet, "/old/{id}", "/api/v2/items/{id}")
At the lower level,
addRoute and
addRedirect register routes and
redirects directly. Registering the same route twice raises a Defect.