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
}
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)
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
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
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)
changes: