<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Raphael Okunlola</title>
    <description>The latest articles on DEV Community by Raphael Okunlola (@topman).</description>
    <link>https://dev.to/topman</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F938295%2F133f2287-85b3-4d3e-9387-2a2a8ed3ff25.jpeg</url>
      <title>DEV Community: Raphael Okunlola</title>
      <link>https://dev.to/topman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/topman"/>
    <language>en</language>
    <item>
      <title>I Was Tired of Maintaining 3 Files for 1 API Endpoint. So I Built Axiomify.</title>
      <dc:creator>Raphael Okunlola</dc:creator>
      <pubDate>Fri, 08 May 2026 12:11:50 +0000</pubDate>
      <link>https://dev.to/topman/i-was-tired-of-maintaining-3-files-for-1-api-endpoint-so-i-built-axiomify-3pfe</link>
      <guid>https://dev.to/topman/i-was-tired-of-maintaining-3-files-for-1-api-endpoint-so-i-built-axiomify-3pfe</guid>
      <description>&lt;h3&gt;
  
  
  17 Days Later: Axiomify v5 Is Live
&lt;/h3&gt;

&lt;p&gt;On April 21 I published a post about why I built Axiomify — a Node.js framework where your Zod schema is your validation, your TypeScript types, and your OpenAPI documentation. One definition. No drift.&lt;/p&gt;

&lt;p&gt;That was 17 days ago. Here's what I've shipped since.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changed between v4 and v5
&lt;/h2&gt;

&lt;p&gt;The first post described the architecture and the problem it solves. v5 is about making that architecture faster, more correct, and production-ready.&lt;/p&gt;

&lt;h3&gt;
  
  
  v5 adds multi-core clustering — and it's built to actually work.
&lt;/h3&gt;

&lt;p&gt;Most Node.js clustering tutorials will send you in the wrong direction. The standard approach uses Node's default &lt;code&gt;SCHED_RR&lt;/code&gt; mode, where the primary process accepts every TCP connection and forwards it to a worker via IPC. One thread in the hot path. Add more workers, add more IPC overhead — the primary is still the bottleneck.&lt;/p&gt;

&lt;p&gt;Axiomify's &lt;code&gt;listenClustered()&lt;/code&gt; skips that entirely. Each worker binds its own socket directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schedulingPolicy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SCHED_NONE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// before any fork&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reusePort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;        &lt;span class="c1"&gt;// in each worker&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SCHED_NONE&lt;/code&gt; stops Node from intercepting worker listen calls. &lt;code&gt;reusePort&lt;/code&gt; lets each worker own its socket. The Linux kernel distributes connections without any user-space coordination — zero IPC in the request hot path.&lt;/p&gt;

&lt;p&gt;Results on a real 8-core machine (co-located loadgen — dedicated loadgen would show higher numbers):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Adapter&lt;/th&gt;
&lt;th&gt;1 worker&lt;/th&gt;
&lt;th&gt;2 workers&lt;/th&gt;
&lt;th&gt;Gain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;@axiomify/http&lt;/td&gt;
&lt;td&gt;35,800 req/s&lt;/td&gt;
&lt;td&gt;57,200 req/s&lt;/td&gt;
&lt;td&gt;+60%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@axiomify/fastify&lt;/td&gt;
&lt;td&gt;21,300 req/s&lt;/td&gt;
&lt;td&gt;35,200 req/s&lt;/td&gt;
&lt;td&gt;+65%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every adapter — Express, Fastify, Hapi, HTTP, and the new native uWS adapter — supports &lt;code&gt;listenClustered()&lt;/code&gt; out of the box. Crash recovery with exponential backoff, graceful SIGTERM drain, and zero-downtime rolling restart via &lt;code&gt;kill -USR2 &amp;lt;primary-pid&amp;gt;&lt;/code&gt; are included.&lt;/p&gt;

&lt;h3&gt;
  
  
  The validator was doing double the work.
&lt;/h3&gt;

&lt;p&gt;Every validated request was running two full passes: AJV for structural validation, then unconditionally &lt;code&gt;schema.parse()&lt;/code&gt; again. The second pass is only needed for schemas with &lt;code&gt;.transform()&lt;/code&gt;, &lt;code&gt;.default()&lt;/code&gt;, or &lt;code&gt;.coerce&lt;/code&gt; — but for a plain object schema with no transforms, it was pure waste on every single request.&lt;/p&gt;

&lt;p&gt;v5 walks the schema once at startup to detect whether transforms exist. If not, the second pass is skipped entirely. The result is a &lt;strong&gt;15–25% throughput improvement&lt;/strong&gt; on validated routes — which is most routes in a typical API.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's new
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;@axiomify/native&lt;/strong&gt; — a uWebSockets.js adapter. The first post listed Express, Fastify, Hapi, and Node.js http as adapter options. Native uWS is now a fifth option for when you need maximum throughput from a single process:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Req/s&lt;/th&gt;
&lt;th&gt;p99&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET /users/:id/posts/:postId&lt;/td&gt;
&lt;td&gt;83,947&lt;/td&gt;
&lt;td&gt;20ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET /ping&lt;/td&gt;
&lt;td&gt;73,511&lt;/td&gt;
&lt;td&gt;26ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST /echo (JSON body)&lt;/td&gt;
&lt;td&gt;54,720&lt;/td&gt;
&lt;td&gt;30ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same API. Same Zod schemas. Same &lt;code&gt;useOpenAPI()&lt;/code&gt; call. You swap &lt;code&gt;HttpAdapter&lt;/code&gt; for &lt;code&gt;NativeAdapter&lt;/code&gt; and the performance changes. Everything else stays identical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;@axiomify/security&lt;/strong&gt; — XSS protection, HTTP Parameter Pollution normalisation, SQL injection heuristics, and prototype pollution blocking. &lt;code&gt;useSecurity(app, options)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;@axiomify/fingerprint&lt;/strong&gt; — server-side request fingerprinting with confidence scoring. Bot detection and fraud signals without client-side JavaScript.&lt;/p&gt;

&lt;p&gt;That takes the monorepo from 17 packages to 20.&lt;/p&gt;




&lt;h2&gt;
  
  
  The things that don't show up in benchmarks
&lt;/h2&gt;

&lt;p&gt;The first post focused on developer experience and architecture. What I spent the last 17 days on — alongside the new features — is the kind of work that makes a framework trustworthy rather than just interesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test coverage: ~70% → 91.6%.&lt;/strong&gt; 462 tests across 51 test files. Cross-adapter parity tests run the same test suite against all five adapters, guaranteeing identical behaviour regardless of which one you use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CodeQL analysis on every push.&lt;/strong&gt; Security vulnerabilities caught before they reach main.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenSSF Scorecard compliance.&lt;/strong&gt; Branch protection requiring reviews before merge, least-privilege GitHub Actions token permissions, Dependabot for automatic dependency updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crash circuit breaker.&lt;/strong&gt; If 5 workers crash within 30 seconds, the primary aborts with a clear error message instead of spinning in a respawn loop burning CPU on a broken config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero-downtime rolling restart.&lt;/strong&gt; &lt;code&gt;kill -USR2 &amp;lt;primary-pid&amp;gt;&lt;/code&gt; restarts workers one at a time. Each replacement comes fully up before the next worker is terminated.&lt;/p&gt;

&lt;p&gt;None of this is glamorous. All of it matters if you're running something real.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where this is going
&lt;/h2&gt;

&lt;p&gt;The current dispatcher overhead versus a bare adapter is around 25% — hook iteration, compiled-state lookup, pipeline execution per request. That's acceptable for most workloads. The next major milestone closes it by generating a compiled handler function per route at startup, inlining the validation, params, and serialization directly.&lt;/p&gt;

&lt;p&gt;This is only possible because of the schema-first architecture described in the first post — the framework has a complete Intermediate Representation of every route at registration time. It's the natural endgame of the "one schema, everything derived" idea.&lt;/p&gt;




&lt;p&gt;Axiomify is on npm. The clustering works. The validation is faster. The test suite is thorough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @axiomify/core @axiomify/openapi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub: &lt;a href="https://github.com/OTopman/axiomify" rel="noopener noreferrer"&gt;github.com/OTopman/axiomify&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What would make you actually switch frameworks? Curious what the real friction is — drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
