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
restmacro.
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()
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
restmacro in depth. - CORS and metrics — cross-origin support and Prometheus metrics.
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.
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:
| Argument | Default | Description |
|---|---|---|
serverIdent | PrestoIdent | value of the Server response header |
serverFlags | {NotifyDisconnect} | Chronos HttpServerFlags |
socketFlags | {ReuseAddr} | listening socket flags |
maxConnections | -1 (unlimited) | connection cap |
bufferSize | 4096 | per-connection buffer |
httpHeadersTimeout | 10.seconds | header read timeout |
maxHeadersSize | 8192 | max request header bytes |
maxRequestBodySize | 1_048_576 | max request body bytes |
requestErrorHandler | nil | custom error handler (see below) |
errorType | cstring | error 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 usableContent-Type.Unexpected— a catch-all.
defaultResponse() returns Presto's built-in response and is handy as a
fallback.
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()
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
- Declaring client procedures
- Creating a client
- Calling an endpoint
- Return types
- Error handling
- Connection handling
- Overloading
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}inendpointfills that path segment; - an argument named
body(or starting withbody) becomes the request body — only allowed forPOST/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
| Pragma | Purpose |
|---|---|
endpoint : "/path/{x}" | request path, with optional patterns |
meth : MethodGet | HTTP 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— encodeseq[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 type | Result |
|---|---|
a value type T (e.g. string, int, a custom type) | the decoded body; a non-2xx status raises RestResponseError |
RestStatus | just the HTTP status code; the body is consumed |
RestPlainResponse | status, content type, headers, and raw data: seq[byte] |
RestResponse[T] | status, content type, and the decoded data: T |
RestHttpResponseRef | the 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
OPTIONShandler for every route you add, so CORS preflight requests are answered without any extra code; - adds
Access-Control-Allow-Originto responses when the request'sOriginmatches the configured value.
The matching rules are:
allowedOrigin = some("*")allows any origin and echoes*back.- Otherwise the request
Originmust match the configured value. A matching response also setsVary: Originto avoid cache poisoning. The configured value may be given with or without anhttp:///https://scheme. - A request carrying more than one
Originheader is rejected with400 Bad Request.
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.