Skip to content

About

A unified and lightweight web application framework for multiple platforms.

Resources

Stars

9 stars

Watchers

1 watching

Forks

Repository files navigation

Handlers.js

Handlers.js is a lightweight web application framework driven by a simple, practical request / response abstraction. Write once, run everywhere — on Node.js, Deno, Bun, txiki.js, and Service Workers / Cloudflare Workers.

This README covers installation, a first application, platform support, benchmark numbers and the development commands. The documentation explains routing, handlers, error handling and the full API.

Installation

Install the package with npm install handlers.js or yarn add handlers.js. The default export is the rootRouter class, so new App() creates an independent router.

# Use Yarn
yarn add handlers.js
# Use NPM
npm install handlers.js

Quick Start

This application answers three routes. Run it with node app.ts on Node.js 22 or later, then request http://localhost:8080/, /users/42 and POST /api.

import App from 'handlers.js';

const app = new App();

app.binding(
    '/',
    app.create('GET', async () => 'Home'),
);
app.binding(
    '/users/:id',
    app.create('GET', async (req) => `user ${req.params.id}`),
);
app.binding(
    '/api',
    app.create('POST', async (req) => ({ received: req.body })),
);

app.useMappingAdapter();
app.listen(8080);

create() wraps the returned value automatically: string → plain text, object → JSON, undefined → 204 No Content, and a response instance is passed through as-is. For sub-routers, error handling and the full API, see the documentation.

Features

  • One request/response model — platform details stay behind a single abstraction, so business code never touches native APIs.
  • Automatic adapter selection — useMappingAdapter() detects the runtime and picks the matching platform adapter.
  • Middleware-style handler chain — bind several responders to one route; a responder that returns nothing acts as middleware.
  • Built-in middleware — cors, logger, bodyLimit, timeout, etag and secureHeaders ship with the framework (import { middleware } from 'handlers.js').
  • Streaming helpers — SSE (streamSSE/formatSSE/parseSSE) and streamText for chunked text delivery (import { helper } from 'handlers.js'). Every invocation owns its headers and stream state; text streams use bounded prefetch and propagate cancellation to their source. See lifecycle and SSE metadata rules.
  • Compiled JSON helpers — helper.createJSON() can compile request validators and response serializers once, or accept precompiled functions. See the Ajv and fast-json-stringify example; JSON parsing still uses JSON.parse().
  • Existing-source AOT CLI — build an ordinary TypeScript or JavaScript entry into one ES module for Node.js, Deno, Bun or txiki.js. Eligible static paths are precompiled, and the report explains the runtime work that remains. See AOT usage and limits.
  • Build tooling included — the package includes TypeScript and Webpack for the Node-hosted compiler, plus path-to-regexp and txiki's Bluebird support. JSON compiler packages remain optional application dependencies.

Build an Existing Application into One File

The handlers CLI turns an existing TypeScript or JavaScript application into one ES module for Node.js, Deno, Bun or txiki.js. The application keeps its imports, routers, handlers, closures and startup code, and needs no manifest or additional API. Install handlers.js in the application project and run its local executable:

npx --no-install handlers check app.ts --target node
npx --no-install handlers build app.ts --target node --outfile build/server.mjs
node build/server.mjs

check analyzes and reports without writing a file or running the application. build writes the artifact atomically and leaves an earlier artifact in place when compilation fails. --target accepts node, deno, bun and txiki; the same entry is built once per target and executed with node, deno run, bun or tjs run. See Existing-source AOT CLI for the options, report fields, diagnostics and the work that remains at runtime. Build tooling can use the handlers.js/compiler entry with check(), build() and formatReport() instead of the executable.

Platform Support

useMappingAdapter() detects every platform in this table. Each row names the adapter it selects.

Platform Auto-detected Adapter Notes
Node.js (≥ 22) ✅ NodePlatformAdapter built-in http module
Service Worker ✅ SWPlatformAdapter fetch event listener
Cloudflare Workers ✅ SWPlatformAdapter use the cfworker build
Deno ✅ DenoPlatformAdapter built-in HTTP server
txiki.js ✅ TxikiPlatformAdapter requires bluebird Promise polyfill
Bun ✅ BunPlatformAdapter native Bun.serve; shared Fetch conversion

Performance

Handlers.js runs the same code on multiple runtimes with no native dependencies. The numbers below were measured on the same development machine with ApacheBench (ab -n 30000 -c 100, no keep-alive, best of two rounds). Benchmark on your own target environment before drawing conclusions; results vary with hardware, runtime version and workload.

Framework (runtime) GET / GET /user/:id GET /json
handlers.js (Node.js) 10,435 9,981 10,588
handlers.js (Deno) 14,313 11,027 14,123
handlers.js (txiki.js) 3,231 3,121 2,715
Express 5 7,660 7,380 7,100
Hono (Node.js) 11,807 13,193 11,482
Fastify 5 13,440 14,446 13,032

requests/second, higher is better. All six rows were measured back-to-back in one continuous window, so the load is shared across them — only the relative order is meaningful.

On this machine, handlers.js on Node.js is roughly 35–50% faster than Express across all three route shapes. It reaches about 76–92% of Hono and Fastify, which ran without compiled request validation or response serialization in that historical benchmark; the results do not measure helper.createJSON(). On Deno, which runs on Deno's native Hyper-based server, it reaches the same range as Hono and Fastify. txiki.js is an embedded, single-threaded runtime, so its throughput reflects that role; it fits deployments where memory is constrained or startup time matters. Handlers.js is a thin multi-platform layer over each runtime's native HTTP stack, not a zero-copy schema engine.

Routing and adapter overhead. The router uses a compact prefix tree when the index-entry count is strictly greater than 16. Each route contributes its safe prefix-alternative count, including duplicates, or one fallback entry if any alternative has no safe prefix. Whole edge strings keep nodes only at branch points and prefix endpoints, and child maps and entry arrays are allocated as needed. Candidate lookup compares whole edges along the ASCII-folded pathname, then deduplicates and sorts matching and fallback routes by registration order. The index is built lazily on the first dispatch and rebuilt on the next dispatch after additions, including for small tables that use linear dispatch.

Node streaming reuses listeners for each response, Fetch adapters copy request headers with Headers.forEach(), and txiki reuses the chunk-length prefix for consecutive nonempty chunks of the same size. txiki uses native globalThis.Promise to interrupt blocked Web-stream writes while keeping sequential output and failure cleanup. The historical table above does not measure these optimizations. See routing and platform behavior for their contracts.

Keep these points in mind when reading the numbers:

  • Compact storage bounds node count by prefix count, but edge splitting and retained strings, nodes and maps still cost construction time and memory.
  • Operation counts and isolated microbenchmarks do not establish universal HTTP throughput gains.
  • Measured results vary by runtime and workload and include adverse CPU or throughput observations.

Third-party numbers below come from different hardware and tools, so do not compare them against the table above. They are for reference only:

Source Environment Reported (req/s)
Fastify official benchmarks (2026-06) hello-world Fastify 42,257 / Hono 38,893
TechPlained (2026) 4-core/8GB Ubuntu 24.04, autocannon 100 conn/30s, JSON route Express 14,800 / Fastify 42,600 / Hono 38,900
nodewire (2026-05) 4 vCPU/8GB droplet, Node 24.14, autocannon 200 conn/60s, Postgres + Prisma, 4KB JSON Express 21,000 / Fastify 114,000 / Hono 128,000 / Hono+Bun 188,000 / Elysia+Bun 205,000
Cadence blog (2026-05) hello-world Express 8–10K / Fastify ~15K / Hono ~62K

Documentation

Chinese documentation is available in README.zh.md and docs/zh.

Contributing

Use the latest Node.js 22.x or 24.x and the Yarn 4 version declared in package.json. The development dependencies require Node.js 22.13+ on the 22.x line or Node.js 24+; the library's Node.js ≥ 22 runtime declaration is unchanged. With Corepack installed, run:

corepack yarn install --immutable
corepack yarn exec tsc --noEmit
NODE_ENV=development BUILD_TARGET=node corepack yarn exec webpack
node ./dist/main.node.js  # run the Node.js demo (default :8080)
corepack yarn test:node   # Node.js tests
corepack yarn test:deno   # Deno tests (requires Deno)
corepack yarn build       # build all platform bundles

After building the demo, run PORT=9090 node ./dist/main.node.js to choose another port. The demo's /whoami endpoint returns the request headers as JSON text with a text/plain content type, and includes the resolved request IP in x-request-ip.

See Building & Testing for demo port fallback rules, standalone Yarn setup, TypeScript compatibility, Jest diagnostics and release checks.

License

MIT

About

A unified and lightweight web application framework for multiple platforms.

Resources

Stars

9 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages