npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@oggy-org/roachjs

v0.0.1

Published

The fastest, simplest HTTP framework for Node.js

Readme

npm license stars ci

Why RoachJS?

You know that cockroach that just crossed your kitchen floor? You blinked and it was gone. No hesitation. No overhead. No seventeen-layer middleware pipeline to decide which direction to scuttle.

That's the energy.

Express has been around since 2010. It's reliable in the way a Buick is reliable — it gets you there, eventually, after burning through half your server budget on redundant object allocations. Fastify improved things considerably, but somewhere between the plugin encapsulation docs and the lifecycle hooks diagram, you lost an afternoon you'll never get back.

RoachJS is built on uWebSockets.js — a C++ HTTP layer that treats microseconds like they matter. One runtime dependency. Zero plugin system. Zero configuration ceremony. You write routes. They handle requests. The benchmarks section below shows what that looks like in practice.

We named it after cockroaches because cockroaches have survived every extinction event in Earth's history. Your framework should survive a traffic spike.

Install

npm install @oggy-org/roachjs

Quick Start

import roach from '@oggy-org/roachjs'

const app = roach()

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

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

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

app.get('/files/*', (req, res) => {
  res.send(req.params['*'])
})

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

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

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

app.listen(3000, () => {
  console.log('RoachJS running on port 3000')
})

That's a working server with routing, params, wildcards, middleware, and error handling. No additional configuration files. No plugins to install.

Documentation

What follows is the complete API reference. Every method, every option, every pattern — with runnable examples.

Routing

Register handlers for any HTTP method. Each registration returns the app, so you can chain if you want to.

app.get('/users', (req, res) => res.json([]))

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

app.put('/users/:id', (req, res) => {
  res.json({ updated: true, id: req.params.id })
})

app.delete('/users/:id', (req, res) => {
  res.status(204).end()
})

app.patch('/users/:id', (req, res) => {
  res.json({ patched: true, id: req.params.id })
})

app.options('/users', (req, res) => {
  res.set('Allow', 'GET, POST, OPTIONS').status(204).end()
})

app.head('/users', (req, res) => {
  res.status(200).end()
})

app.all('/health', (req, res) => {
  res.send('OK')
})

Route Parameters

Named parameters use the :param syntax. Wildcards use * and capture everything after the prefix.

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

app.get('/users/:userId/posts/:postId', (req, res) => {
  res.json({
    userId: req.params.userId,
    postId: req.params.postId
  })
})

app.get('/static/*', (req, res) => {
  const filePath = req.params['*']
  res.send(`Serving: ${filePath}`)
})

The router is a hand-written radix tree. Static routes resolve in O(1) via a hash map cache. Parametric routes resolve in O(log n). Wildcards are checked last.

Query Strings

Query parameters are parsed lazily. If you never access req.query, zero CPU is spent parsing the query string.

// GET /search?q=roachjs&page=2&limit=20
app.get('/search', (req, res) => {
  res.json({
    query: req.query.q,
    page: req.query.page,
    limit: req.query.limit
  })
})

Request Body

When the Content-Type header is application/json, the body is automatically parsed as JSON on first access. Like query strings, parsing is lazy.

app.post('/api/data', (req, res) => {
  const parsed = req.body
  const raw = req.rawBody

  res.status(201).json({ received: parsed })
})

If the content type is not JSON, req.body returns the raw body as a UTF-8 string. The original Buffer is always available via req.rawBody.

Middleware

Three levels of middleware: global (every request), path-scoped (matching prefix), and route-level (specific routes).

// Global — runs on every request
app.use((req, res, next) => {
  req.startTime = Date.now()
  next()
})

// Path-scoped — only runs for /api/* routes
app.use('/api', (req, res, next) => {
  const auth = req.get('authorization')
  if (!auth) {
    return res.status(401).json({ error: 'Missing authorization header' })
  }
  next()
})

// Route-level — extra arguments before the final handler
const rateLimit = (req, res, next) => {
  // your rate limiting logic here
  next()
}

app.get('/expensive', rateLimit, (req, res) => {
  res.json({ data: 'worth protecting' })
})

Calling next() advances to the next middleware. Calling next(err) skips the remaining chain and triggers the error handler. Async middleware is supported — thrown errors and rejected promises are caught automatically.

Response Methods

res.send('Hello')                         // text/plain, string or Buffer
res.json({ key: 'value' })               // application/json, auto-serialized
res.status(201).json({ created: true })   // chainable status code
res.set('X-Request-Id', 'abc123')         // set response header, chainable
res.type('text/html')                     // set Content-Type, chainable
res.redirect('/login')                    // 302 redirect by default
res.redirect('/moved', 301)               // permanent redirect
res.status(204).end()                     // end with no body

All terminal methods (send, json, redirect, end) guard against double-sends. Attempting to send a response twice throws a ResponseAlreadySentError instead of silently corrupting the connection.

Router Groups

Group related routes under a shared prefix using roach.router().

import roach from '@oggy-org/roachjs'

const app = roach()

const api = roach.router()
api.get('/ping', (req, res) => res.send('pong'))
api.get('/users', (req, res) => res.json([]))
api.post('/users', (req, res) => res.status(201).json(req.body))

app.use('/api', api)

app.listen(3000)

Requests to /api/ping, /api/users, etc. are routed to the sub-router. Each sub-router has its own independent radix tree.

Error Handling

Set a global error handler with app.onError(). Async errors are caught automatically — no unhandled promise rejections.

app.onError((err, req, res) => {
  console.error(`${req.method} ${req.path} failed:`, err.message)
  res.status(err.statusCode || 500).json({
    error: err.message,
    code: err.code
  })
})

app.get('/risky', async (req, res) => {
  const data = await someOperationThatMightFail()
  res.json(data)
})

If someOperationThatMightFail() rejects, the error handler receives the rejection reason. No try/catch needed in your route handlers.

Not Found

Customize what happens when no route matches.

app.onNotFound((req, res) => {
  res.status(404).json({
    error: `Cannot ${req.method} ${req.path}`
  })
})

TypeScript

Type definitions are available in a separate package:

npm install -D @oggy-org/roachjs-types

Request Reference

| Property | Type | Description | |----------|------|-------------| | req.method | string | HTTP method (uppercase) | | req.path | string | URL path without query string | | req.params | object | Route parameters | | req.query | object | Parsed query string (lazy) | | req.headers | object | Request headers | | req.body | * | Parsed body (lazy, JSON auto-parsed) | | req.rawBody | Buffer\|null | Raw request body buffer | | req.ip | string | Client IP address | | req.get(name) | function | Get header by name |

Response Reference

| Method | Returns | Description | |--------|---------|-------------| | res.send(data) | void | Send string or Buffer | | res.json(data) | void | Send JSON response | | res.status(code) | res | Set status code (chainable) | | res.set(name, value) | res | Set response header (chainable) | | res.type(contentType) | res | Set Content-Type (chainable) | | res.redirect(url, code?) | void | Redirect (default 302) | | res.end() | void | End response with no body |

Contributors

| jackson-peg | | :---: |

Star History

Benchmarks

Benchmarks are lies until proven otherwise. Here is exactly how we run ours so you can reproduce them yourself.

Benchmark Environment & Logs

Benchmarks were executed on the following system configuration. You can view the full execution logs in the section below.

| Component | Specification | | :--- | :--- | | CPU | AMD EPYC 7763 64-Core Processor | | Cores | 4 | | RAM | 16GB | | OS | Linux 6.14.0-1017-azure (x64) | | Node.js | v20.20.0 (LTS) |

╔══════════════════════════════════════════════╗
║       RoachJS Benchmark Suite                ║
╚══════════════════════════════════════════════╝

System: AMD EPYC 7763 64-Core Processor (4 cores) | 16GB RAM
OS:     Linux 6.14.0-1017-azure (x64)
Node:   v20.20.0

Duration: 10s | Connections: 10 | Pipelining: 1

══════════════════════════════════════════════════
  Benchmark 1: Hello World
══════════════════════════════════════════════════

  --- RoachJS ---

  --- Fastify ---

  --- Express ---

  Results:
    roachjs    28,645 req/sec  (avg 0.02ms, p99 1ms)
    fastify    16,163 req/sec  (avg 0.02ms, p99 1ms)
    express    6,144 req/sec  (avg 1.09ms, p99 3ms)

══════════════════════════════════════════════════
  Benchmark 2: JSON Response
══════════════════════════════════════════════════

  --- RoachJS ---

  --- Fastify ---

  --- Express ---

  Results:
    roachjs    26,210 req/sec  (avg 0.02ms, p99 0ms)
    fastify    13,661 req/sec  (avg 0.02ms, p99 1ms)
    express    5,967 req/sec  (avg 1.06ms, p99 3ms)

══════════════════════════════════════════════════
  Benchmark 3: Route Params + Body Parsing
══════════════════════════════════════════════════

  --- RoachJS ---

  --- Fastify ---

  --- Express ---

  Results:
    roachjs    18,958 req/sec  (avg 0.02ms, p99 1ms)
    fastify    11,312 req/sec  (avg 0.04ms, p99 1ms)
    express    4,629 req/sec  (avg 2.06ms, p99 4ms)

══════════════════════════════════════════════════
  Benchmark 4: Middleware Chain
══════════════════════════════════════════════════

  --- RoachJS ---

  --- Fastify ---

  --- Express ---

  Results:
    roachjs    27,037 req/sec  (avg 0.02ms, p99 0ms)
    fastify    13,856 req/sec  (avg 0.02ms, p99 1ms)
    express    6,090 req/sec  (avg 1.05ms, p99 3ms)

══════════════════════════════════════════════════
  SUMMARY
══════════════════════════════════════════════════
  Hello World      RoachJS:   28,645 | 1.8x Fastify | 4.7x Express
  JSON Response    RoachJS:   26,210 | 1.9x Fastify | 4.4x Express
  Params + Body    RoachJS:   18,958 | 1.7x Fastify | 4.1x Express
  Middleware       RoachJS:   27,037 | 2.0x Fastify | 4.4x Express

Hello World

The most basic test. A single route returning a plain text response. This isolates raw HTTP throughput from any application logic.

RoachJS:

import roach from '@oggy-org/roachjs'
const app = roach()
app.get('/', (req, res) => res.send('Hello World'))
app.listen(3000)

Fastify:

import Fastify from 'fastify'
const app = Fastify()
app.get('/', (req, reply) => reply.send('Hello World'))
app.listen({ port: 3000 })

Express:

import express from 'express'
const app = express()
app.get('/', (req, res) => res.send('Hello World'))
app.listen(3000)

JSON Response

A route that returns a JSON object. Tests JSON serialization overhead on top of raw throughput.

RoachJS:

app.get('/json', (req, res) => {
  res.json({ message: 'Hello World', timestamp: Date.now() })
})

Fastify:

app.get('/json', (req, reply) => {
  reply.send({ message: 'Hello World', timestamp: Date.now() })
})

Express:

app.get('/json', (req, res) => {
  res.json({ message: 'Hello World', timestamp: Date.now() })
})

Route Parameters + JSON Body Parsing

A more realistic test. A POST route with a URL parameter and a JSON body that gets parsed and returned. This tests the router, body parser, and response serializer working together.

RoachJS:

app.post('/users/:id', (req, res) => {
  const { id } = req.params
  const body = req.body
  res.json({ id, ...body })
})

Fastify:

app.post('/users/:id', (req, reply) => {
  const { id } = req.params
  const body = req.body
  reply.send({ id, ...body })
})

Express:

app.use(express.json())
app.post('/users/:id', (req, res) => {
  const { id } = req.params
  const body = req.body
  res.json({ id, ...body })
})

Middleware Chain

Tests the overhead of running multiple middleware functions before reaching the route handler. Each middleware adds a header to the request.

RoachJS:

const mw = (req, res, next) => { req.headers['x-test'] = 'true'; next() }
app.use(mw)
app.use(mw)
app.use(mw)
app.get('/middleware', (req, res) => res.send('done'))

Fastify:

app.addHook('onRequest', async (req) => { req.headers['x-test'] = 'true' })
app.addHook('onRequest', async (req) => { req.headers['x-test'] = 'true' })
app.addHook('onRequest', async (req) => { req.headers['x-test'] = 'true' })
app.get('/middleware', (req, reply) => reply.send('done'))

Express:

const mw = (req, res, next) => { req.headers['x-test'] = 'true'; next() }
app.use(mw)
app.use(mw)
app.use(mw)
app.get('/middleware', (req, res) => res.send('done'))

Run it yourself

git clone https://github.com/oggy-org/roachjs
cd roachjs
npm install
npm run benchmark
npm run benchmark:svg

Results will be printed to the console. SVGs will be regenerated in assets/.