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

Quickstart

Presto is an asynchronous REST framework for Nim, built on top of the Chronos async I/O library. It gives you a request router, an HTTP (and HTTPS) server to run it on, and an HTTP client that is generated for you at compile time.

A typical Presto program does three things:

  • declares routes on a RestRouter, where request parameters are automatically unmarshalled into Nim types;
  • serves that router with a RestServerRef (or embeds it into an existing Chronos HTTP server as middleware);
  • optionally talks to a server using client procedures produced by the rest macro.

Installation

Install Presto with Nim's package manager, Nimble:

nimble install presto

To depend on Presto from your own package, add it to your .nimble file:

requires "presto"

Presto requires Nim 1.6.18 or newer and pulls in chronos, chronicles, metrics, results and stew as dependencies.

A minimal server

The following program starts a server that answers GET / with a plain-text greeting.

Presto calls a user-supplied decodeString to turn path and query parameters into Nim values. Provide one for every type you accept as a parameter.

proc decodeString*(t: typedesc[string], value: string): RestResult[string] =
  ok(value)

RestRouter.init always takes a validation callback. It is invoked for every {pattern} segment in a route; returning 0 accepts the value and any other value rejects the request. If you don't need validation, return 0 for everything.

proc validate(pattern: string, value: string): int = 0

With those in place, declare a route with the api macro and serve the router:

var router = RestRouter.init(validate)

router.api(MethodGet, "/") do () -> RestApiResponse:
  RestApiResponse.response("Hello World", Http200, "text/plain")

let server = RestServerRef.new(router, initTAddress("127.0.0.1:9000")).get()
server.start()
runForever()

Note

RestRouter.init always requires a validation callback — see Routing for details on patterns and validation.

Calling it from a client

Presto can generate a client procedure straight from a signature. The rest macro reads the endpoint and meth pragmas and produces an async proc that performs the request and decodes the response.

The client lives in the presto/client module, and calls a user-supplied decodeBytes to turn the response body into a Nim value.

import pkg/presto/[common, client]

proc decodeBytes*(t: typedesc[string], value: openArray[byte],
                  contentType: Opt[ContentTypeData]): RestResult[string] =
  var res: string
  if len(value) > 0:
    res = newString(len(value))
    copyMem(addr res[0], unsafeAddr value[0], len(value))
  ok(res)

proc getRoot(): string {.rest, endpoint: "/", meth: MethodGet.}

proc main() {.async.} =
  let client = RestClientRef.new(initTAddress("127.0.0.1:9000"))
  echo await client.getRoot()
  await client.closeWait()

waitFor main()

API Docs

Where to go next

  • Routing — declaring routes, path/query/body parameters, responses, and the encode/decode contract.
  • Server — server options, lifecycle, error handling, and TLS.
  • Middleware — embedding a router into an existing Chronos HTTP server.
  • Client — the rest macro in depth.
  • CORS and metrics — cross-origin support and Prometheus metrics.

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.

Server

Once you have a router, you serve it with a RestServerRef. The server owns a Chronos HTTP server, dispatches incoming requests through the router, and turns the RestApiResponse your handlers return into real HTTP responses.

Creating a server

RestServerRef.new returns a Result, so unwrap it with get() (or handle the error). At minimum it needs a router and an address to bind to.

let server = RestServerRef.new(router, address).get()

Common options

new accepts many keyword arguments; the most useful ones are:

ArgumentDefaultDescription
serverIdentPrestoIdentvalue of the Server response header
serverFlags{NotifyDisconnect}Chronos HttpServerFlags
socketFlags{ReuseAddr}listening socket flags
maxConnections-1 (unlimited)connection cap
bufferSize4096per-connection buffer
httpHeadersTimeout10.secondsheader read timeout
maxHeadersSize8192max request header bytes
maxRequestBodySize1_048_576max request body bytes
requestErrorHandlernilcustom error handler (see below)
errorTypecstringerror type of the returned Result

The errorType parameter lets you choose whether construction failures are reported as cstring (the default) or string:

let res = RestServerRef.new(router, address, errorType = string)
if res.isErr():
  echo "failed to start: ", res.error()

Lifecycle

server.start()              # begin accepting connections
echo server.state           # Running | Stopped | Closed
echo server.localAddress()  # actual bound address (useful with port 0)
await server.stop()         # stop accepting new connections
await server.drop()         # drop pending connections
await server.closeWait()    # stop and release all resources

The relevant procedures are start, stop, drop, closeWait, join, state and localAddress.

A typical test or short-lived program brackets its work with start and closeWait:

server.start()
try:
  # … issue requests …
  discard
finally:
  await server.closeWait()

Handling errors

Some failures happen before or around your handler: a malformed request, an unknown route, or an undecodable body. By default Presto answers these with a bare status code (400, 404, …). Supply a requestErrorHandler to customize them.

The handler receives a RestRequestError describing what went wrong and the HttpRequestRef, and returns an HttpResponseRef.

proc onError(kind: RestRequestError,
             request: HttpRequestRef): Future[HttpResponseRef] {.
    async: (raises: [CancelledError]).} =
  try:
    case kind
    of RestRequestError.Invalid:
      await request.respond(Http400, "invalid request")
    of RestRequestError.NotFound:
      await request.respond(Http404, "no such endpoint")
    of RestRequestError.InvalidContentBody:
      await request.respond(Http400, "bad body")
    of RestRequestError.InvalidContentType:
      await request.respond(Http400, "bad content-type")
    of RestRequestError.Unexpected:
      defaultResponse()
  except HttpError:
    defaultResponse()
let server = RestServerRef.new(
  router, initTAddress("127.0.0.1:8080"), requestErrorHandler = onError).get()

The RestRequestError cases are:

  • Invalid — the request line or path could not be parsed.
  • NotFound — no route matched.
  • InvalidContentBody — the body could not be read.
  • InvalidContentType — a body was sent without a usable Content-Type.
  • Unexpected — a catch-all.

defaultResponse() returns Presto's built-in response and is handy as a fallback.

Note

The error handler covers framework errors. Errors your own handler wants to report should be returned as RestApiResponse.error(...) instead (see Building responses).

Serving over HTTPS

For TLS, use SecureRestServerRef from presto/secureserver. It behaves exactly like RestServerRef—same routing, same responses—but additionally takes a TLS private key and certificate.

let
  key = TLSPrivateKey.init(myKeyPem)
  cert = TLSCertificate.init(myCertPem)
  server = SecureRestServerRef.new(
    router, initTAddress("127.0.0.1:8443"), key, cert).get()

server.start()

SecureRestServerRef supports the same lifecycle procedures (start, stop, drop, closeWait, join, state, localAddress) and the same errorType option as the plain server, plus a secureFlags: set[TLSFlags] argument for TLS-specific behavior.

Middleware

Instead of running a Presto router with a dedicated RestServerRef, you can plug it into an existing Chronos HTTP server as middleware. This is useful when you already have a Chronos HttpServerRef (perhaps serving static files or a non-REST protocol) and want to add REST endpoints to it, or when you want to compose several routers into one server.

A RestServerMiddlewareRef wraps a router and behaves like any other Chronos HttpServerMiddlewareRef: for each request it tries to match a route, and if none matches it passes the request on to the next handler in the chain.

Wrapping a router

Create the middleware from a router with RestServerMiddlewareRef.new, then pass it to HttpServerRef.new via the middlewares argument.

var router = RestRouter.init(validate)
router.api(MethodGet, "/api/{id}") do (id: int) -> RestApiResponse:
  RestApiResponse.response("item " & $id.get())

let restMiddleware = RestServerMiddlewareRef.new(router)

The middleware also accepts an optional errorHandler of the same RestRequestErrorHandler type used by the server.

The fall-through handler

The plain Chronos HttpServerRef still needs a process callback—the final handler that runs when no middleware matched the request. This is where you serve everything that isn't a REST route (or return a 404).

proc process(r: RequestFence): Future[HttpResponseRef] {.
    async: (raises: [CancelledError]).} =
  if r.isOk():
    let request = r.get()
    if request.uri.path == "/health":
      try:
        await request.respond(Http200, "ok")
      except HttpWriteError as exc:
        defaultResponse(exc)
    else:
      defaultResponse()   # -> 404
  else:
    defaultResponse()

let server = HttpServerRef.new(
  initTAddress("127.0.0.1:8080"), process,
  middlewares = [restMiddleware]).get()
server.start()

Chaining multiple routers

You can install several middlewares. They are tried in order; the first router that has a matching route handles the request, and anything unmatched falls through to the next middleware and finally to the process callback.

var
  apiRouter = RestRouter.init(validate)
  adminRouter = RestRouter.init(validate)

apiRouter.api(MethodGet, "/api/{id}") do (id: int) -> RestApiResponse:
  RestApiResponse.response("api")

adminRouter.api(MethodPost, "/admin/{id}") do (
    id: int, contentBody: Option[ContentBody]) -> RestApiResponse:
  RestApiResponse.response("admin")

let server = HttpServerRef.new(
  initTAddress("127.0.0.1:8080"), process,
  middlewares = [
    RestServerMiddlewareRef.new(apiRouter),
    RestServerMiddlewareRef.new(adminRouter)
  ]).get()

Tip

Because matching considers the HTTP method as well as the path, two routers can share the same path and be selected by the request method — for example one router handling GET /resource and another handling POST /resource.

Client

Presto ships with an HTTP client that is generated from your procedure signatures. You describe what a request looks like (its endpoint, method, and parameters) and the rest macro writes the code that builds the request, sends it, follows redirects, and decodes the response into a Nim value. The client lives in the presto/client module.

Declaring client procedures

Annotate a procedure with {.rest.} and describe the request with pragmas. The proc must have no body; the macro supplies one.

proc getUser(id: int): string {.rest, endpoint: "/users/{id}".}
proc createUser(body: string): string {.
     rest, endpoint: "/users", meth: MethodPost.}

Parameter names drive how each argument is used:

  • a name that matches a {pattern} in endpoint fills that path segment;
  • an argument named body (or starting with body) becomes the request body — only allowed for POST/PUT/PATCH/DELETE;
  • any other argument becomes a query-string parameter. Option[T] makes it optional; seq[T] repeats the key.

Client-side values are converted with your encodeString (path/query) and encodeBytes (body) procedures, and responses are decoded with decodeBytes. See the encode/decode contract.

Pragmas

PragmaPurpose
endpoint : "/path/{x}"request path, with optional patterns
meth : MethodGetHTTP method (defaults to GET)
accept : "application/json"value of the Accept header (defaults to application/json)
connection : {Dedicated}connection handling (see Connection handling)
metrics / metricsTypes : {...}enable client metrics for the call (see CORS and metrics)

Creating a client

RestClientRef.new can be constructed from a TransportAddress, from an HttpAddress, or from a URL string. The URL form returns a Result because it resolves the host up front.

# from a transport address
let client = RestClientRef.new(initTAddress("127.0.0.1:8080"))

# from a URL (returns a Result)
let client2 = RestClientRef.new("http://api.example.com/").get()

# with flags
let client3 = RestClientRef.new(
  initTAddress("127.0.0.1:8080"),
  HttpClientScheme.NonSecure,
  flags = {RestClientFlag.CommaSeparatedArray})

Two RestClientFlags tune behavior:

  • CommaSeparatedArray — encode seq[T] query parameters as a single comma-delimited value instead of repeating the key.
  • ResolveAlways — perform DNS resolution on every request rather than caching the resolved address.

Calling an endpoint

The generated proc is async and takes your declared parameters plus three extra keyword arguments: restContentType, restAcceptType, and extraHeaders.

let client = RestClientRef.new(initTAddress("127.0.0.1:8080"))

let user = await client.getUser(42)

let created = await client.createUser(
  body = "{\"name\":\"Ada\"}",
  restContentType = "application/json",
  extraHeaders = @[("Authorization", "Bearer secret")])

await client.closeWait()

restAcceptType overrides the accept pragma at the call site and understands quality weights, e.g. "app/type1;q=1.0,app/type2;q=0.1".

Return types

The proc's return type selects how much of the response you get back and how errors are handled.

Return typeResult
a value type T (e.g. string, int, a custom type)the decoded body; a non-2xx status raises RestResponseError
RestStatusjust the HTTP status code; the body is consumed
RestPlainResponsestatus, content type, headers, and raw data: seq[byte]
RestResponse[T]status, content type, and the decoded data: T
RestHttpResponseRefthe raw response for manual/streaming reads

Examples:

proc getStatus(): RestStatus {.rest, endpoint: "/health".}
proc getRaw(): RestPlainResponse {.rest, endpoint: "/blob".}
proc getTyped(): RestResponse[int] {.rest, endpoint: "/count".}
proc getStream(body: string): RestHttpResponseRef {.
     rest, endpoint: "/download", meth: MethodPost, accept: "*/*".}

Reading a streaming response:

let resp = await client.getStream("query")
let reader = resp.getBodyReader()
let chunk = await reader.read()
await reader.closeWait()
await resp.closeWait()

Error handling

When a value-returning proc receives a non-2xx status it raises RestResponseError, which carries the details of the failed response.

try:
  let user = await client.getUser(99999)
  discard user
except RestResponseError as exc:
  echo exc.status        # e.g. 404
  echo exc.message       # response body as text
  echo exc.contentType

Other exceptions the generated procs may raise include RestEncodingError (a parameter or body failed to encode), RestDnsResolveError (host resolution failed), RestCommunicationError (transport/HTTP failure), and RestDecodingError (the response body failed to decode). CancelledError propagates as usual.

Connection handling

By default connections are pooled and reused. The connection pragma changes this per call:

  • {} or {Dedicated} — keep the connection open for reuse.
  • {Close} — close the connection after the request.
proc oneShot(): RestPlainResponse {.
     rest, endpoint: "/once", connection: {Close}.}

Overloading

Because the macro produces ordinary procedures, you can overload the same endpoint with different parameter sets, e.g. a queryless variant and one that takes filters, and let Nim's overload resolution pick between them at the call site.

CORS and metrics

CORS

Browsers restrict cross-origin requests unless the server opts in with the appropriate Access-Control-Allow-Origin header. Presto handles this at the router level: pass an allowedOrigin when you initialize the router with RestRouter.init.

var router = RestRouter.init(
  validate, allowedOrigin = some("https://app.example.com"))

When allowedOrigin is set, Presto:

  • automatically registers an OPTIONS handler for every route you add, so CORS preflight requests are answered without any extra code;
  • adds Access-Control-Allow-Origin to responses when the request's Origin matches the configured value.

The matching rules are:

  • allowedOrigin = some("*") allows any origin and echoes * back.
  • Otherwise the request Origin must match the configured value. A matching response also sets Vary: Origin to avoid cache poisoning. The configured value may be given with or without an http:// / https:// scheme.
  • A request carrying more than one Origin header is rejected with 400 Bad Request.

Note

allowedOrigin is applied by the router and the server together. If you serve a router as middleware, the same CORS behavior applies.

Metrics

Presto can expose operational metrics through the nim-metrics library. Metrics collection is a compile-time feature: build with -d:metrics to enable it. Without that flag, all the metrics machinery compiles away to nothing.

Server metrics

The server can record, per endpoint:

  • the number of responses, labelled by HTTP status;
  • the time taken to prepare each response.

It also maintains global counters for processed, missing (404), and invalid (400) requests.

Per-route recording is opt-in through the metrics variants of the routing macros. Pass a set of RestServerMetricsType values (Status, Response, or the combined RestServerMetrics) to metricsApi:

router.metricsApi(MethodGet, "/items/{id}",
                  {RestServerMetricsType.Status}) do (
    id: int) -> RestApiResponse:
  RestApiResponse.response("item")

# record both status counts and response timing
router.metricsApi(MethodGet, "/report", RestServerMetrics) do () -> RestApiResponse:
  RestApiResponse.response("ok")

A rawMetricsApi variant exists for raw handlers, mirroring rawApi.

Client metrics

On the client, metrics are enabled per procedure with the metrics pragma. The client can record DNS resolution time, connection time, request time, response time, and response status, each labelled by an endpoint name.

# use the endpoint path as the metric label
proc getItem(id: int): RestPlainResponse {.
     rest, endpoint: "/items/{id}", metrics.}

# override the label
proc getItem2(id: int): RestPlainResponse {.
     rest, endpoint: "/items/{id}", metrics: "items_by_id".}

# select which metrics to collect
proc getItem3(id: int): RestPlainResponse {.
     rest, endpoint: "/items/{id}", metrics,
     metricsTypes: {RestClientMetricsType.ResponseTime,
                    RestClientMetricsType.Status}.}

The available RestClientMetricsType values are ResolveTime, ConnectTime, RequestTime, ResponseTime, and Status; RestClientMetricsAllTypes is the full set and is used when metricsTypes is omitted.