How I’m writing web apps with Nim in 2026

Jan 4, 2026

A few things have changed in the Nim webdev community since I last wrote in 2021. The main update is that Jester is effectively in maintenance mode, and has some issues with the latest version of Nim. All of the other libraries/frameworks are still being actively developed (or are stable on the latest versions of Nim), which is a healthy sign.

If you’re someone who would much rather browse through code, I have a small sample app using this stack that can be found here.

Web server

Since Jester is gone, I’ve switched to mummy. It uses threads (instead of async/await), which leads to simpler code and even easier debugging due to stack traces. The author of mummy wrote a post on the Nim forum that I recommend reading through on how he’s using it in production. It’s remarkably fast and stable - I’ve been running it on my web server uninterrupted for several months, and there have been absolutely no memory leaks or crashes in that time. I also used it to build sakura.arhamjain.com, which saw traffic spikes during Sakura-Con (in Seattle), and never had any issues with scaling.

import mummy, mummy/routers

proc indexHandler(request: Request) =  
  var headers: HttpHeaders
  headers["Content-Type"] = "text/plain"  
  request.respond(200, headers, "Hello, World\!")

var router: Router  
router.get("/", indexHandler)

let server = newServer(router)  
echo "Serving on http://localhost:8080"  
server.serve(Port(8080))

For routing, I wrote my own (very small) library, inspired by Roda instead of using the router that Mummy ships with. I find that specifying a routing tree is more natural for resource based URL schemes, and it makes it easy to add hooks and middleware without introducing callbacks/registration.

import mummy, rody

let handler = route:  
  headers["Content-Type"] = "text/html"  
    at "/":   
      get:  
        resp “hello world”

echo "Serving on http://localhost:8080"  
newServer(handler).serve(Port(8080))

ORM

I’ve switched from Norm to debby. This isn’t because of any issues with Norm, it’s mostly because I like using libraries written by treeform and guzba - if a Nim library ends in “y”, there’s a 90% chance one of them wrote it. Debby is much more hands off compared to Norm - it can generate tables and query them, but it doesn’t handle joins or most complicated SQL syntax. Instead, it specializes in mapping the results of a SQL query to a Nim object in a typesafe way.

import debby/sqlite
let db = openDatabase("cars.db")

type Car = ref object  
  id: int  
  make: string  
  model: string  
  year: int

db.createTable(Car)

var car = Car(
  make: "Chevrolet",
  model: "Camaro Z28",
  year: 1970  
)
db.insert(car)
car = db.get(Car, car.id)
car.year = 1971
db.update(car)
db.delete(car)

It handles the common cases succinctly, and avoids any magic, making it more difficult to accidentally end up with performance issues. It also means that you’re never waiting on an ORM to add support for some arcane feature or syntax - just write SQL directly. I’m also using SQLite for my apps, I don’t have nearly enough traffic to worry about scaling past that.

UI

I’ve switched from HTMX to Unpoly. HTMX is a fantastic library and I deeply respect the work that the authors have put into it. I used HTMX for several years and was pretty happy with it (when compared to traditional SPA frameworks and NPM). Both libraries are prioritizing HTML of the wire, but the key difference is that Unpoly is more “use-case” focused, while HTMX is more focused on “building-blocks”.

For example, Unpoly’s doc will walk you through how to handle form validation, dependent form fields, and other common journeys. HTMX is much more hands off by comparison - they give you the building blocks (hx-get, hx-post) and leave it to the developer to decide how to handle form validation.

Unpoly is more opinionated, but the opinions are designed for classic (2000-2010) web development. For example, Unpoly only issues GET and POST requests, making progressive enhancement (in case the user is not using JS) much easier to keep.

Here’s a list of magical Unpoly features I’ve found useful that would require more thinking and potentially Javascript to replicate with HTMX:

  • Automatic progress bars if a request is taking too long
  • Adding a class to a nav item if the URL matches the address bar
  • Placeholder loading (showing a UI skeleton while a request works)
  • First class form validation
  • Automatic caching for GET requests
  • Modal dialogs and drawers

It’s also much more "enterprisey" - the author takes backwards compatibility and migrations seriously, even providing a polyfill for new versions to maintain existing behavior.

Unpoly isn’t perfect. For starters, it’s a much larger package than HTMX - 60kb gzipped vs 16kb gzipped. You do get a lot more functionality, but it’s definitely larger than I would like. The API surface is also massive compared to HTMX - probably over 100 attributes, though many attributes are for configuring sub parameters and reused between different features.

Templating

For templating, I use source code filters. Source code filters are actually a very old way of doing string templating in Nim that's built into the language. Here's a quick example:

#? stdtmpl(toString = "safe")  
#proc createGamePage(count: int): string  
<p>You have played the game $count   
#if $count == 1:  
time  
#else  
times  
#end  
</p>  
#end

Source code filters are turned into Nim code directly by the compiler, which means that they’re just a bunch of string allocations. This ends up being quite fast at runtime, and during compile time as no macros are used to process the code.

Conclusion

Nim is a slower moving language than most others with a smaller ecosystem, which has both upsides and downsides. Other than Jester, I didn’t need to change any part of this stack. Generally I’ll rewrite a small app every time I’m evaluating a different library to see whether the tradeoffs are worth it. I’ve used the stack above for over a year now and it’s been incredibly quick to develop while having fantastic performance.

I’ve used all of the above in a small toy app (mentioned at the start), in case the code makes it easier to understand what’s going on. I hope this post was helpful!

https://arhamjain.com/feed.xml