DEV Community

Cover image for I Built a Node.js HTTP Framework Faster Than Fastify From Scratch. Here is How.
Jackson
Jackson

Posted on

I Built a Node.js HTTP Framework Faster Than Fastify From Scratch. Here is How.

I want to preface this by saying I did not set out to build a framework. Nobody wakes up and thinks "today I will reinvent the wheel." But sometimes the wheel is too slow, and sometimes the other wheel is too complicated, and sometimes you just start writing code at 11pm and three months later you have a framework named after cockroaches.

This is that story.


The Problem

I have been building Node.js APIs for a while. For most of that time I used Express. Express is comfortable. It feels like home. It is also, when you actually benchmark it, kind of slow.

So I moved to Fastify. Fastify is genuinely fast and the team behind it has done incredible work. But every time I started a new project I found myself spending the first hour reading plugin documentation instead of writing actual code. The fastify-plugin wrapping, the encapsulation model, the schema-based serialization — all of it is powerful, but it has a learning curve that I kept bumping into.

I wanted something in between. Fast like Fastify. Simple like Express. Zero time spent reading documentation before you can write your first route.

So I built RoachJS.


What is RoachJS?

RoachJS is a minimal HTTP framework for Node.js. It has:

  • Zero runtime dependencies
  • A custom Radix Tree router written from scratch
  • An Express-style API that anyone can pick up in five minutes
  • uWebSockets.js under the hood for raw speed

The name comes from the cockroaches in the cartoon Oggy and the Cockroaches. Cockroaches survive everything. They never slow down. They just run. That felt right for a framework.


The Architecture

uWebSockets.js — The Speed Foundation

The single biggest decision in building RoachJS was choosing uWebSockets.js as the HTTP layer. uWebSockets.js is a C++ HTTP server with Node.js bindings. It is one of the fastest HTTP servers ever written for any language, and it makes the built-in Node.js http module look slow by comparison.

Most frameworks — including Express — sit on top of Node's built-in http module. That is not a bad choice, but it means you are inheriting its performance ceiling. uWebSockets.js has a much higher ceiling, and RoachJS is built directly on top of it.

The Radix Tree Router

This is the part I am most proud of building.

A naive router — like the one Express uses internally — checks routes linearly. You have 50 routes, it might check all 50 before finding a match. That is O(n). Fine for small apps, painful at scale.

A Radix Tree (also called a Patricia Trie) compresses shared prefixes into shared nodes. So /users, /users/:id, and /users/:id/posts all share the same root node. Route matching becomes O(log n).

Here is a simplified version of how the matching works:

/**
 * Match a path against the router tree
 * @param {RadixNode} node - Current node in the tree
 * @param {string} path - Remaining path to match
 * @param {Object} params - Accumulated route parameters
 * @returns {{ handler: Function, params: Object } | null}
 */
function match(node, path, params) {
  // Exact match
  if (node.path === path) {
    return { handler: node.handler, params }
  }

  for (const child of node.children) {
    // Parametric segment like :id
    if (child.isParam) {
      const end = path.indexOf('/', 1)
      const segment = end === -1 ? path.slice(1) : path.slice(1, end)
      const remaining = end === -1 ? '' : path.slice(end)
      params[child.name] = segment
      const result = match(child, remaining, params)
      if (result) return result
      delete params[child.name]
    }

    // Wildcard
    if (child.isWildcard) {
      params['*'] = path.slice(1)
      return { handler: child.handler, params }
    }

    // Static segment
    if (path.startsWith(child.path)) {
      const result = match(child, path.slice(child.path.length), params)
      if (result) return result
    }
  }

  return null
}
Enter fullscreen mode Exit fullscreen mode

The full implementation handles route conflicts, throws clear errors when you register duplicate routes, and supports nested router groups.

The Middleware Chain

Middleware in RoachJS works exactly like Express. You pass (req, res, next) and call next() to continue or next(err) to jump to the error handler.

The implementation keeps the chain as flat as possible. No unnecessary function wrapping, no closure allocation on every request. The goal is that a request going through three middleware functions should cost almost nothing compared to a request with no middleware.


The API

Here is everything you need to know to use RoachJS:

import roach from '@oggy-dev/roachjs'

const app = roach()

// Basic routing
app.get('/', (req, res) => {
  res.send('Hello from RoachJS!')
})

// Route parameters
app.get('/users/:id', (req, res) => {
  res.json({ id: req.params.id })
})

// POST with body parsing
app.post('/users', (req, res) => {
  const body = req.body
  res.status(201).json({ created: true, data: body })
})

// Middleware
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`)
  next()
})

// Router groups
const api = roach.router()
api.get('/ping', (req, res) => res.send('pong'))
app.use('/api', api)

// Error handling
app.onError((err, req, res) => {
  res.status(500).json({ error: err.message })
})

// Not found
app.onNotFound((req, res) => {
  res.status(404).json({ error: 'Route not found' })
})

app.listen(3000)
Enter fullscreen mode Exit fullscreen mode

If you know Express, you already know this. The only difference is it runs significantly faster.


The Benchmarks

All tests run on Node.js v20 LTS, 10 concurrent connections, 10 second duration using autocannon. Every framework tested with its recommended production configuration.

Hello World — raw throughput

Framework Req/sec
RoachJS 28,000
Fastify 15,000
Express 6,000

JSON Response

Framework Req/sec
RoachJS 25,000
Fastify 13,000
Express 5,000

Route Params + Body Parsing

Framework Req/sec
RoachJS 18,000
Fastify 14,000
Express 4,000


Roughly 2x faster than Fastify across the board. Roughly 5x faster than Express.

These are honest numbers. If you find a configuration that makes any of them fairer, open an issue and I will rerun.

You can reproduce these yourself:

git clone https://github.com/oggy-org/roachjs
cd roachjs
npm install
npm run benchmark
Enter fullscreen mode Exit fullscreen mode

What I Learned Building This

Building a router from scratch is harder than it looks. The Radix Tree sounds simple until you start handling edge cases — overlapping parametric and static routes at the same depth, wildcard conflicts, trailing slash normalization. It took three rewrites to get right.

uWebSockets.js is incredibly fast but has a learning curve. The API is nothing like Node's http module. Adapting it to feel like a normal Node.js server took a lot of careful wrapping.

Zero dependencies is a real constraint. Every time I wanted to reach for a utility library I had to stop and write it myself. It slows you down initially but the result is a codebase you understand completely.

Body parsing is where performance gets complicated. The third benchmark narrows the gap with Fastify because JSON parsing is expensive and doing it lazily only helps so much. This is the area I am focused on optimizing next.


What is Next

RoachJS is at v0.0.1. The foundation is solid but there is a lot to build:

  • TypeScript definitions
  • Static file serving
  • Cookie support
  • HTTPS support
  • Better body parsing performance
  • More benchmark scenarios

Try It

npm install @oggy-dev/roachjs
Enter fullscreen mode Exit fullscreen mode

The repo is at github.com/oggy-org/roachjs. If you find a bug, open an issue. If you want to contribute, read CONTRIBUTING.md. The project has a CI bot called Roach Manager that handles issue triage and PR checklists automatically so contributions are smooth.

I would genuinely love feedback — especially from people who build production APIs. What is missing? What feels wrong? What would make you actually switch to this?


Built with chaos and love by oggy-org.

Top comments (1)

Collapse
 
jacksonpeg profile image
Jackson

changes:

npm install @oggy-org/roachjs 
Enter fullscreen mode Exit fullscreen mode