Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Routing

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)

Note

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())

Path length limit

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.

ProcedureUsed byPurpose
decodeString(t: typedesc[T], value: string): RestResult[T]serverdecode a path/query parameter
decodeBytes(t: typedesc[T], value: openArray[byte], contentType: Opt[ContentTypeData]): RestResult[T]clientdecode a response body
encodeString(value: T): RestResult[string]clientencode a path/query parameter
encodeBytes(value: T, contentType: string): RestResult[seq[byte]]clientencode 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)

Tip

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.

Note

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.