Jester gives Nim a Sinatra DSL, and tells you to hide behind a proxy
A sinatra-like web framework for Nim.
At a glance
- What is it?
- A single-file web framework for Nim with a routes macro, @-delimited path parameters, regex patterns and a cond template. The README's own deployment advice is that it is not hardened against HTTP exploits and belongs behind a reverse proxy.
- Who is it for?
- Use Jester if you are writing a small Nim service, an internal tool or a webhook receiver and want route handlers in a few lines, since the entire interface is one macro and one implicit request object. Do not expose it to the internet on its own: the README states it is not yet hardened against HTTP security exploits.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 86 days ago.
- What is it written in?
- Mainly Nim, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The whole example fits in five lines
Jester's pitch is a DSL close enough to Sinatra that the README can show it immediately. The example imports htmlgen and jester, opens a routes block, and responds to GET / with a generated heading element.
routes:
get "/":
resp h1("Hello world")The README says to compile and run it from tests/example with nim c -r example.nim, then view it at localhost:5000. Port 5000 is therefore the default you should expect, and it is worth knowing before you wonder why nothing answers on the conventional HTTP port.
The implementation is one main file. The repository has jester.nim at the root alongside a jester/ directory, a jester.nimble manifest, tests/, a changelog.markdown, a todo.markdown and a .gitmodules file. There is no build system beyond Nimble, no generated client and no service layer.
@ replaces : in path parameters, and ? makes a segment optional
The README is explicit that the pattern syntax is almost identical to Sinatra's, with one real difference: @ is used where Sinatra uses a colon.
get "/hello/@name":
resp "Hello " & @"name"A named segment matches any value that does not contain a slash, and the captured value is read back with the same @"name" syntax. Adding a question mark makes the segment optional, which matches the empty form of the path as well.
The trailing detail is the one that catches people. Jester will not match /hello when the leading slash has not been made optional itself, so a truly optional trailing segment needs the pattern written as /hello/?@name?. Anyone porting Sinatra routes across will hit this the first time a redirect ends up at the bare path.
The README also states plainly that the pattern support is more limited than Sinatra's and that there are no wildcard patterns. So a catch-all route does not exist in this syntax.
Regex routes write captures into request.matches
A route path may also be a regular expression, using Nim's re literal syntax. The subpattern captures land in request.matches, indexed from zero.
get re"^\/([0-9]{2})\.html$":
resp request.matches[0]That pattern matches a two-digit URL such as /15.html and gives back 15. The matches field is a fixed-size array of MaxSubpatterns strings rather than a sequence, so a pattern with many groups is bounded at compile time.
Combining named segments and regex in the same pattern is not something the README describes, so treat the two as separate mechanisms: use @name for simple captures and a regex literal when you need grouping or anchors.
cond falls through to the next matching route
Conditions exist, and the README limits them to a simple cond template. The behaviour is worth reading twice because it is not a filter that ends the request: cond passes execution to the next matching route when the condition is false.
get "/@name":
cond @"name" == "daniel"
resp "Correct, my name is daniel."
get "/@name":
resp "No, that's not my name."So the second route with the same pattern acts as the else branch. The ordering rule matters because routes execute in declaration order, so a broad pattern declared before a narrow one will shadow it.
The README warns about that ordering explicitly when it explains that routes run in the order they are declared and that you should be careful when halting. Combined with the fact that there is no wildcard syntax, ordering is the main tool you have for disambiguating routes.
The implicit request object carries the whole request
Every route body has an implicit request variable, and the README prints its definition because the shape matters. The object holds params as a StringTableRef carrying both pattern parameters and the query string, matches as the regex capture array, body as a string that is only populated for POST, headers as a case-insensitive StringTableRef, and formData as MultiData for multipart/form-data.
It also carries connection-level fields: port, host, ip, secure, reqMeth as an HttpMethod such as HttpGet or HttpPost, and settings. Three entries exist specifically for running behind something else. appName is set by the user in run and is overridden by the SCRIPT_NAME SCGI parameter. pathInfo is path without appName, and path is the raw path.
Those three are the framework's whole answer to deployment behind a proxy or a mount point. If your reverse proxy strips or rewrites a prefix, appName and pathInfo are what let a route see a consistent path.
Respond in one of four ways: a resp function, by setting body, headers and/or status and returning, the redirect function, or the attachment function.
Static files have an others-readable requirement that catches people
Static files are served from ./public by default, and setStaticDir overrides that. The mapping is direct, so ./public/css/style.css is served at the /css/style.css path.
The README attaches a note that looks minor and is not: Jester only serves files that are readable by others, and on Unix and Linux you ensure that with chmod o+r on the file. The reason is that the server process is not the user running the chmod, so a file created with restrictive permissions simply is not served, and the symptom is a 404 with no useful error.
Cookies are set with setCookie, which takes a name, a value and an expiry; the example uses daysForward(5) for a five-day lifetime. They are read back through request.cookies, which returns a Table[string, string]. The README does not describe signing, encryption or httpOnly defaults for cookies, so treat the values as readable by the client unless you check jester.nim's documentation.
A router block exists for when the macro is in the way
The routes macro is optional. The README describes manual routing, where you write your own match procedure and are pointed at tests/example2.nim for the pattern. The custom router example goes further and shows how to run your own initialisation code and pass dynamic settings into Jester before the async loop starts.
router myrouter:
get "/":
resp "It's alive!"The settings object is built with newSettings, in the example with a port parsed from the first command line argument, and passed to initJester before serve is called. That is the mechanism for binding to a specific port rather than taking the default.
The other example in the README parses a JSON payload from a route parameter and is labelled as a GitHub service hook, with the note that the code is similar to Sinatra's post-receive hook example. It illustrates the common webhook shape rather than adding hook-specific functionality.
The security warning is the first deployment instruction
The README's production advice is unambiguous and appears right after the hello-world example: before deploying, ensure the application runs behind a reverse proxy, because the library is not yet hardened against HTTP security exploits and applications written in it should not be exposed to the public internet.
That statement has been part of the documentation as given here, and it is the single most important line for anyone evaluating the project. A Sinatra-shaped DSL invites people to assume Sinatra's maturity, and this framework is explicitly not at that level.
Maintenance signals are mixed in a way worth recording. The repository is not archived and the last push was on 2026-07-29, so there is recent work on the branch. The releases are older: v0.6.0 on 2023-06-18, v0.5.0 on 2020-10-19, v0.4.3 on 2019-08-12. So commits continue without new tags, and a consumer pinning to v0.6.0 is pinning to code from 2023.
The licence is MIT, in license.txt. The topics list hacktoberfest alongside framework, http, nim and web, which explains some of the contribution history.
Editorial conclusion
Use Jester if you are writing a small Nim service, an internal tool or a webhook receiver and want route handlers in a few lines, since the entire interface is one macro and one implicit request object. Do not expose it to the internet on its own: the README states it is not yet hardened against HTTP security exploits. Verify first which port your router binds and that a reverse proxy terminates TLS in front of it, then confirm the jester.nimble version your build resolves against the v0.6.0 tag from 2023-06-18, since the last push was on 2026-07-29 and no newer release tag exists.
Frequently asked questions
Is Jester safe to expose to the internet?
The README says no. It states the library is not yet hardened against HTTP security exploits and that applications written in it should not be exposed to the public internet, and advises running behind a reverse proxy before deploying to production.
How do I match a path parameter in Jester?
Use @ where Sinatra uses a colon, as in get "/hello/@name", and read the captured value back with @"name". A segment matches anything without a slash. Adding a question mark makes the segment optional, and you may need to make the leading slash optional too, as in /hello/?@name?.
Does Jester support wildcard routes?
No. The README states that Jester's pattern support is more limited than Sinatra's and that there are no wildcard patterns. Routes also execute in declaration order, so ordering is how you disambiguate overlapping routes.
Why are my static files not served by Jester?
Jester only serves files that are readable by others. On Unix and Linux you ensure that with chmod o+r on the file, for example chmod o+r ./public/css/style.css.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/dom96-jester)Was this useful?