<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged nim</title>
<link>https://coderlegion.com/tag/nim</link>
<description>Powered by Question2Answer</description>
<item>
<title>A Tiny Nim Microservice, Three Real Bugs, and What They Taught Me About My Own Mummy Fork</title>
<link>https://coderlegion.com/27202/a-tiny-nim-microservice-three-real-bugs-and-what-they-taught-me-about-my-own-mummy-fork</link>
<description>&lt;p&gt;I recently needed something small: a &lt;code&gt;POST /api/contact&lt;/code&gt; endpoint for my portfolio site at &lt;code&gt;https://www.isaiah.name.ng&lt;/code&gt; that takes a name, email, and message, validates it, and emails it to me. The kind of thing you'd normally knock out without thinking twice.&lt;/p&gt;
&lt;p&gt;I decided to build it directly on my own &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/isaiahpeter/adding-openapi-support-to-mummy-a-nim-http-framework-227m&quot;&gt;Mummy fork&lt;/a&gt; — the one with OpenAPI schema generation, typed validation, and composable middleware — partly to actually dogfood it on something real, and partly because I assumed a single-endpoint service would be trivial. It mostly was. But three specific bugs along the way taught me more about the fork's internals than building the fork itself did.&lt;/p&gt;
&lt;h3 id=&quot;the-shape-of-the-service&quot;&gt;The Shape of the Service&lt;/h3&gt;
&lt;p&gt;Nothing exotic: a &lt;code&gt;ContactBody&lt;/code&gt; type with &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, and &lt;code&gt;message&lt;/code&gt;, all required strings. Validation happens in two layers — &lt;code&gt;parseValidatedBody[ContactBody]&lt;/code&gt; checks presence and type against the fork's &lt;code&gt;schemaOf&lt;/code&gt;-generated schema, and then explicit checks enforce non-empty fields, a 2000-character cap, and a hand-rolled email format check. Either layer raising the fork's &lt;code&gt;ValidationError&lt;/code&gt; gets mapped to a clean &lt;code&gt;400 {&quot;error&quot;: ...}&lt;/code&gt; by &lt;code&gt;installDefaultErrorHandler&lt;/code&gt;. A send failure is logged server-side and answered with a generic &lt;code&gt;500&lt;/code&gt; — the caller never sees why an email failed to send, only that it did.&lt;/p&gt;
&lt;p&gt;On top of that: CORS middleware that rejects disallowed origins with &lt;code&gt;403&lt;/code&gt; and answers preflight requests properly, an in-memory rate limiter capping submissions at 5 per IP per 10 minutes, and the fork's &lt;code&gt;loggingMiddleware&lt;/code&gt; as the outermost layer so every request logs method, path, and timing. The OpenAPI docs come free from the same schema the validator uses, served at &lt;code&gt;/docs&lt;/code&gt; with the spec at &lt;code&gt;/openapi.json&lt;/code&gt; — the whole reason I built the fork in the first place.&lt;/p&gt;
&lt;h3 id=&quot;the-pivot-nobody-plans-for-smtp-resend&quot;&gt;The Pivot Nobody Plans For: SMTP → Resend&lt;/h3&gt;
&lt;p&gt;The original plan was plain SMTP via &lt;code&gt;std/smtp&lt;/code&gt;. It compiled fine against the fork. Then I deployed to Render's free tier and every send just hung until timeout — Render blocks outbound SMTP ports (25/465/587) on that tier. Resend's REST API runs over HTTPS:443, which isn't blocked, and needs just one API key. As a bonus, its failures come back as ordinary HTTP errors the handler already catches and logs, instead of the occasional &lt;code&gt;AssertionDefect&lt;/code&gt; that a raw SMTP library can throw and potentially take a worker down with it.&lt;/p&gt;
&lt;p&gt;This is the kind of constraint you only discover by actually deploying, not by reading docs — worth remembering if you're building anything Render-hosted that needs to send mail.&lt;/p&gt;
&lt;h3 id=&quot;three-bugs-that-taught-me-more-than-the-feature-did&quot;&gt;Three Bugs That Taught Me More Than the Feature Did&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Two &lt;code&gt;HttpHeaders&lt;/code&gt; types that look identical and aren't.&lt;/strong&gt;&lt;br&gt;
Mummy imports &lt;code&gt;std/httpcore&lt;/code&gt; aliased internally, and &lt;code&gt;Request.headers&lt;/code&gt; is typed against that aliased import. My module also did a plain &lt;code&gt;import std/httpcore&lt;/code&gt; for the Resend payload types — and suddenly every header operation threw a &quot;type mismatch&quot; between what looked like the same type. Nim's module system treats the two imports as distinct type identities even though they're structurally the same. The fix was narrow: &lt;code&gt;import std/httpcore except HttpHeaders&lt;/code&gt;, so my module only pulled in the pieces it needed for the Resend payload and left the header type alone. This was, by a wide margin, the buggiest hour of the whole build — and a good reminder that &quot;same type, different import path&quot; is a real failure mode in Nim, not a theoretical one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;gcsafe&lt;/code&gt; doesn't care that your global never actually changes.&lt;/strong&gt;&lt;br&gt;
The fork's &lt;code&gt;Middleware&lt;/code&gt; and &lt;code&gt;RequestHandler&lt;/code&gt; types are marked &lt;code&gt;{.gcsafe.}&lt;/code&gt;, which means the compiler's data-race checker gets strict about any global state touched inside a handler — because Mummy dispatches requests across a worker-thread pool, not a single event loop. My CORS middleware read a &lt;code&gt;var&lt;/code&gt; global holding the allowed-origins list, set once at startup and never touched again — and the compiler didn't care that it was logically immutable after init. It only sees a mutable global read from a &lt;code&gt;gcsafe&lt;/code&gt; context. The fix was a &lt;code&gt;{.cast(gcsafe).}&lt;/code&gt; block around that specific read, while the genuinely-shared rate-limit table stayed behind a real &lt;code&gt;Lock&lt;/code&gt; — the same pattern the fork already uses internally for &lt;code&gt;bearerAuthMiddleware&lt;/code&gt;. It's a useful distinction to internalize on a threaded server: &quot;never mutated after startup&quot; and &quot;thread-safe&quot; are not the same claim, and the compiler will only ever check for the second one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. An unregistered route means your middleware never runs — including for the request that most needs it.&lt;/strong&gt;&lt;br&gt;
Browsers send an &lt;code&gt;OPTIONS&lt;/code&gt; preflight before certain cross-origin &lt;code&gt;POST&lt;/code&gt; requests. Mummy only runs its middleware chain for methods that have a registered route — so with no &lt;code&gt;OPTIONS /api/contact&lt;/code&gt; route, Mummy answered preflight with its own 405 before &lt;code&gt;corsMiddleware&lt;/code&gt; ever got a chance to run. The browser never saw the &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header it needed, so the real &lt;code&gt;POST&lt;/code&gt; was blocked client-side despite the server being otherwise correctly configured. The fix was almost comically small: register an &lt;code&gt;OPTIONS /api/contact&lt;/code&gt; route whose handler does nothing but exist, which is enough to route the request into the middleware chain where &lt;code&gt;corsMiddleware&lt;/code&gt; can actually answer it with a &lt;code&gt;204&lt;/code&gt;. Easy to fix once you know it — genuinely confusing the first time a working-looking CORS setup silently fails only on the preflight leg.&lt;/p&gt;
&lt;h3 id=&quot;what-actually-got-verified&quot;&gt;What Actually Got Verified&lt;/h3&gt;
&lt;p&gt;Before calling it done, I ran it locally and checked the specific paths that tend to hide bugs rather than just the happy path: a disallowed origin (&lt;code&gt;403&lt;/code&gt;), a malformed email (&lt;code&gt;400&lt;/code&gt; with the exact field named), a name at the 2000-character boundary (passes) versus one character over (rejected), six rapid submissions from one IP (five succeed, the sixth comes back &lt;code&gt;429&lt;/code&gt; with &lt;code&gt;Retry-After&lt;/code&gt;), and that &lt;code&gt;/health&lt;/code&gt; and &lt;code&gt;OPTIONS&lt;/code&gt; both stay outside the rate limiter so Render's health checks and CORS preflight never get throttled.&lt;/p&gt;
&lt;h3 id=&quot;why-this-was-worth-writing-up&quot;&gt;Why This Was Worth Writing Up&lt;/h3&gt;
&lt;p&gt;None of these three bugs were about the feature — a contact form endpoint is about as unglamorous as backend work gets. They were about the specific intersection of Nim's module aliasing, &lt;code&gt;gcsafe&lt;/code&gt;'s literal (not logical) read of what's safe, and Mummy's routing-before-middleware order. If you're building anything on Mummy — my fork or the original — that touches CORS, thread-shared state, or has more than one import path into &lt;code&gt;std/httpcore&lt;/code&gt;, these three are worth checking for before you hit them the hard way.&lt;/p&gt;
&lt;p&gt;The full service, Dockerfile, and a drop-in &lt;code&gt;script.js&lt;/code&gt; for wiring this into any static contact form are in the repo if you want to see the whole thing end to end.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27202/a-tiny-nim-microservice-three-real-bugs-and-what-they-taught-me-about-my-own-mummy-fork</guid>
<pubDate>Tue, 15 Sep 2026 00:19:50 +0000</pubDate>
</item>
<item>
<title>Adding OpenAPI Support to Mummy, a Nim HTTP Framework</title>
<link>https://coderlegion.com/27091/adding-openapi-support-to-mummy-a-nim-http-framework</link>
<description>&lt;h2 id=&quot;adding-openapi-support-to-mummy-lessons-from-extending-nim-s-thread-per-request-http-server&quot;&gt;Adding OpenAPI Support to Mummy: Lessons from Extending Nim's Thread-Per-Request HTTP Server&lt;/h2&gt;
&lt;p&gt;Mummy is a multi-threaded HTTP 1.1 and WebSocket server for Nim built around a simple, unapologetic idea: skip &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; entirely and let a pool of worker threads handle requests the old-fashioned way — one thread, one request, blocking calls allowed. No &lt;code&gt;Future[T]&lt;/code&gt;, no colored functions, no async stack traces to untangle. For a systems language with real threads and ARC/ORC to make that cheap, it's a compelling trade.&lt;/p&gt;
&lt;p&gt;What Mummy doesn't ship with, though, is any notion of API documentation. There's no built-in way to describe your routes, generate a schema, or hand a frontend team (or your future self) a &lt;code&gt;/docs&lt;/code&gt; page. That gap is what got me building.&lt;/p&gt;
&lt;h3 id=&quot;why-this-gap-matters&quot;&gt;Why This Gap Matters&lt;/h3&gt;
&lt;p&gt;Nim's web ecosystem is small enough that most frameworks are optimized for the author's own use case first. Mummy is optimized for raw throughput and simplicity — and it succeeds at both. But once an API has more than a couple of routes, &quot;read the source to find out what a request body looks like&quot; stops scaling. OpenAPI (formerly Swagger) solves this by generating a machine-readable schema of your API directly from your code, which then powers interactive docs, client SDK generation, and request validation.&lt;/p&gt;
&lt;p&gt;I forked Mummy and built this in as a layer on top, rather than a separate tool that has to be kept in sync by hand.&lt;/p&gt;
&lt;h3 id=&quot;what-i-built&quot;&gt;What I Built&lt;/h3&gt;
&lt;p&gt;The fork adds two core modules — &lt;code&gt;openapi_schema.nim&lt;/code&gt; and &lt;code&gt;openapi_router.nim&lt;/code&gt; — plus an &lt;code&gt;OpenApiRouter&lt;/code&gt; that wraps Mummy's normal routing with metadata: summaries, tags, and response schemas generated via &lt;code&gt;schemaOf&lt;/code&gt;. Every one of the 12 bundled examples now serves a live &lt;code&gt;/openapi.json&lt;/code&gt; and an interactive &lt;code&gt;/docs&lt;/code&gt; page, generated from the same route definitions that handle requests — no separate spec file to maintain.&lt;/p&gt;
&lt;p&gt;From there I added three more pieces that came up naturally once the schema layer existed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Typed path params&lt;/strong&gt; (&lt;code&gt;openapi_params.nim&lt;/code&gt;) — &lt;code&gt;pathParam[T](request, &quot;id&quot;)&lt;/code&gt; pulls a path segment straight out as an &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, or whatever type you declare, instead of manually parsing strings on every handler.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Request body validation&lt;/strong&gt; (&lt;code&gt;openapi_validation.nim&lt;/code&gt;) — &lt;code&gt;parseValidatedBody[T]&lt;/code&gt; checks an incoming JSON body against the same schema that &lt;code&gt;schemaOf&lt;/code&gt; generated for your docs, so the validation and the documentation can't drift apart.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composable middleware&lt;/strong&gt; (&lt;code&gt;openapi_middleware.nim&lt;/code&gt;) — a &lt;code&gt;Middleware&lt;/code&gt; type and a &lt;code&gt;use()&lt;/code&gt; method on the router, with logging and bearer-auth middleware included out of the box.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every feature was compiled and exercised with real HTTP requests, not just &lt;code&gt;nim check&lt;/code&gt;, before I called it done — including converting a wildcard WebSocket route to a named path param and confirming it fixed a small latent bug where the old code read the raw request URI (query string and all) instead of the decoded path.&lt;/p&gt;
&lt;h3 id=&quot;what-the-wider-nim-community-s-experience-with-mummy-adds&quot;&gt;What the Wider Nim Community's Experience With Mummy Adds&lt;/h3&gt;
&lt;p&gt;Reading through discussion around Mummy on the Nim mailing list and forum was useful context while building this. One recurring theme: Mummy's design intentionally keeps some things out of scope in the name of simplicity. A developer comparing thread-per-request servers noted that Mummy's multipart handling, while present, buffers the entire upload into memory rather than streaming it — a deliberate trade-off for simplicity that matters if you're building something like a file-upload service, and pointed to alternatives like Guildenstern for cases needing streaming multipart support.&lt;/p&gt;
&lt;p&gt;That trade-off is directly relevant to schema-driven validation work: &lt;code&gt;parseValidatedBody[T]&lt;/code&gt; inherits the same &quot;load it all, then check it&quot; model. For typical JSON APIs that's a non-issue, but it's a useful thing to know going in if you're planning a Mummy-based service that also needs large file uploads — you may want to keep that path outside the validated-body flow rather than fight the framework's grain.&lt;/p&gt;
&lt;p&gt;It's also worth knowing you're not alone in wanting typed, schema-aware APIs on Mummy — a separate project, Sarcophagus, tackles a similar problem from a different angle: typed request/response encoding across JSON, CBOR, and MessagePack, plus its own Swagger/OpenAPI generation and OAuth2 helpers. Different design choices, same underlying itch in the ecosystem — which suggests this is a real gap worth filling rather than a one-off preference of mine.&lt;/p&gt;
&lt;h3 id=&quot;putting-it-to-use&quot;&gt;Putting It to Use&lt;/h3&gt;
&lt;p&gt;To validate the fork beyond the bundled examples, I built a small Todo API demo using the typed params, validation, and middleware together, and I'm deploying it live on Render as a working reference — a REST API with real request validation and generated docs, running on a threaded model with no &lt;code&gt;async&lt;/code&gt; in sight.&lt;/p&gt;
&lt;h3 id=&quot;where-this-goes-next&quot;&gt;Where This Goes Next&lt;/h3&gt;
&lt;p&gt;The fork is usable today for anyone building JSON APIs on Mummy who wants documentation and validation that can't silently drift from the actual routes. If you're working in the Nim web space and hit the same &quot;no schema, no docs&quot; wall, I'd genuinely like to hear how you're solving it — feel free to reach out.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27091/adding-openapi-support-to-mummy-a-nim-http-framework</guid>
<pubDate>Sun, 13 Sep 2026 22:28:03 +0000</pubDate>
</item>
</channel>
</rss>