New:Microsoft Teams Notifications Are Now Available in Socket.Learn more
Get Started

kenojs

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

kenojs

A modular TypeScript-first server toolkit with typed HTTP routing, WebSocket support, and scalable transport primitives.

latest
Source
npmnpm
Version
0.1.2
Version published
Maintainers
1
Created
Source

Keno

Keno Logo

npm package Node 18.17+ TypeScript first ESM only Transports License MIT

keno is a TypeScript-first server toolkit and modern client stack for building HTTP, WebSocket, and HTTP/2 applications with typed routing, familiar middleware, modular public imports, and scalable transport primitives.

Why Keno Exists

keno was created to keep the small-app experience pleasant without painting larger systems into a corner.

Many Node server stacks feel strong in one dimension and awkward in another:

  • good routing, but no built-in story for WebSockets
  • flexible middleware, but little help for host-aware routing or multi-listener setups
  • good server ergonomics, but a disconnected client story

keno aims to keep one mental model across those layers instead:

  • familiar app.use(...), app.get(...), and app.ws(...)
  • typed route params and request helpers that stay pleasant in TypeScript
  • modular subpath exports for tree-shakeable public imports
  • built-in transport and concurrency primitives when an app grows past a single listener
  • an HTTP client and WebSocket client that follow the same DX-first approach

Highlights

  • Express-like HTTP routing and middleware chaining
  • Typed route params inferred from path patterns
  • Built-in request helpers for JSON, text, cookies, content negotiation, and host metadata
  • Built-in response helpers for JSON, redirects, files, downloads, cookies, links, and cache headers
  • WebSocket routing with protocol negotiation and permessage-deflate
  • WebSocket rooms and heartbeat utilities
  • Fetch-style HTTP client with middleware, retries, timeout, prepared requests, and typed route contracts
  • WebSocket client for browser, Bun, and Node runtimes with a WebSocket implementation
  • Host-aware routing with host(...) and domain(...)
  • One app, multiple listeners through listenMany(...) and keno/multi-server
  • Worker pools for CPU-bound endpoints
  • Thread clustering with reusePort
  • http, https, and http2 transports, including RFC 8441 extended CONNECT for WebSockets over HTTP/2
  • Temporary TLS generation for local secure development
  • Plugin system with built-in heartbeat, request logger, and OpenAPI plugins
  • Public folder-based subpath imports such as keno/client, keno/middleware, and keno/worker-pool
  • Benchmark harness and Autobahn WebSocket conformance runner in-repo

Project Health

  • Contributing guide
  • Code of Conduct
  • Security Policy

Installation

npm install keno

Runtime notes:

  • Server APIs target Node >=18.17.0
  • The package is ESM-only
  • The HTTP client works in fetch-compatible runtimes
  • The WebSocket client works in browsers, Bun, and Node runtimes that provide globalThis.WebSocket, or with an injected constructor

Quick Start

import keno from "keno";

const app = keno();

app.use(keno.requestId());
app.use(keno.securityHeaders());
app.use(keno.json());

app.get("/users/:id", (request, response) => {
  response.json({
    id: request.params.id,
    requestId: response.locals.requestId,
    transport: request.transport,
  });
});

app.ws("/events", (socket) => {
  socket.sendText("ready");

  socket.on("text", (message) => {
    socket.sendText(`echo:${message}`);
  });
});

await app.listen(3000).ready();

HTTP Client Quick Start

import { createHttpClient } from "keno/client";

type User = {
  id: string;
  name: string;
};

const client = createHttpClient({
  baseURL: "https://api.myapp.com",
  headers: {
    authorization: "Bearer token",
  },
  timeout: 3000,
});

const user = await client
  .get("/users/:id", {
    params: {
      id: "42",
    },
  })
  .expectOk()
  .json<User>();

const prepared = client.get("/users/:id", {
  params: {
    id: "42",
  },
}).prepare();

const raw = await prepared.fetch();
console.log(user, raw.status);

Design Model

keno is centered on a small set of runtime building blocks:

  • keno() and createApp()
    • Create an application instance with HTTP, WebSocket, plugin, and transport support.
  • KenoRouter
    • Compose route trees and mount them under prefixes or host patterns.
  • KenoRequest
    • Runtime request view with params, query, cookies, body parsing, and content negotiation helpers.
  • KenoResponse
    • Chainable response builder for JSON, files, downloads, redirects, cookies, and headers.
  • KenoWebSocket
    • Typed event-based WebSocket connection wrapper.
  • HttpClient
    • Fetch-style client with fluent builders, middleware, retries, and typed route contracts.
  • KenoWebSocketClient
    • Isomorphic reconnect-capable WebSocket client.

This keeps the everyday API small while still leaving room for host-aware routing, secure transports, concurrency, and shared contracts.

Public Imports

keno keeps the public surface modular through folder-based subpath exports:

  • App and routing: keno, keno/application, keno/router, keno/request, keno/response, keno/types
  • Middleware and plugins: keno/middleware, keno/plugins
  • Clients and realtime: keno/client, keno/websocket
  • Transport and concurrency: keno/certificates, keno/multi-server, keno/thread-cluster, keno/worker-pool

Example:

import { createApp } from "keno/application";
import { json, requestId } from "keno/middleware";
import { createHttpClient } from "keno/client";
import { createWorkerPool, threaded } from "keno/worker-pool";

const app = createApp();
const client = createHttpClient();

app.use(requestId());
app.use(json());

Routing And Middleware

The core router keeps a familiar shape on purpose:

import keno from "keno";

const app = keno();
const api = keno.Router();

app.use(async (request, response, next) => {
  const startedAt = Date.now();

  await next();

  console.log(request.method, request.path, response.statusCode, Date.now() - startedAt);
});

api.get("/users/:id", (request, response) => {
  response.json({
    id: request.params.id,
  });
});

app.use("/api", api);
app.host("api.local.test", api);

Built-in middleware today:

  • cors(...)
  • json(...)
  • text(...)
  • requestId(...)
  • securityHeaders(...)
  • serveStatic(...) and static(...)

Request And Response Helpers

Request helpers include:

  • request.params, request.query, request.cookies, and request.body
  • request.get(...), request.header(...), request.cookie(...), and request.param(...)
  • request.accepts(...), request.acceptsLanguages(...), request.acceptsEncodings(...), and request.acceptsCharsets(...)
  • request.is(...), request.xhr, and request.hasBody
  • request.origin, request.host, request.hostname, request.ip, request.secure, and request.transport
  • await request.buffer(), await request.text(), and await request.json<T>()

Response helpers include:

  • response.status(...), response.type(...), response.json(...), and response.send(...)
  • response.cookie(...) and response.clearCookie(...)
  • response.redirect(...)
  • await response.sendFile(...) and await response.download(...)
  • response.links(...), response.vary(...), response.location(...), and response.attachment(...)
  • response.set(...), response.append(...), response.remove(...), and response.sendStatus(...)

WebSockets

WebSocket routes use the same routing model as HTTP routes:

import keno from "keno";

const app = keno();
const rooms = keno.createWebSocketRooms();

app.ws("/chat/:room", (socket, request) => {
  const room = request.params.room;

  rooms.join(room, socket);
  socket.sendText(`joined:${room}`);

  socket.on("text", (message) => {
    rooms.broadcast(room, {
      message,
      room,
      type: "message",
    });
  });

  socket.on("close", () => {
    rooms.leave(socket);
  });
});

Realtime features currently available:

  • app.ws(...) handlers with typed route params
  • protocol negotiation during the handshake
  • permessage-deflate support
  • createWebSocketRooms() for room membership and broadcast
  • createWebSocketHeartbeat() for keepalive management
  • createWebSocketClient() in keno/client
  • WebSocket over http, https, and http2

If your Node runtime does not expose globalThis.WebSocket, inject one explicitly:

import { WebSocket } from "ws";
import { createWebSocketClient } from "keno/client";

const client = createWebSocketClient("ws://127.0.0.1:3000/events", {
  WebSocket,
});

Plugins

Applications can register reusable behavior through plugins:

import keno from "keno";

const app = keno();

await app.register(keno.heartbeatPlugin, {
  details: () => ({
    region: "local",
    transport: "http",
  }),
  name: "public-api",
});

await app.register(keno.openApiPlugin, {
  title: "Public API Docs",
  document: {
    openapi: "3.1.0",
    info: {
      title: "Public API",
      version: "1.0.0",
    },
    paths: {},
  },
});

Built-in plugins:

  • heartbeatPlugin
  • openApiPlugin
  • requestLoggerPlugin

Custom plugins can be defined with definePlugin(...).

HTTP Client

The HTTP client is designed to feel modern without getting in the way.

Fluent requests

import { createHttpClient } from "keno/client";

const client = createHttpClient({
  baseURL: "https://api.myapp.com",
});

const created = await client
  .post("/users", {
    body: {
      name: "Keno",
    },
  })
  .expect(201)
  .json<{ id: string; name: string }>();

Middleware, retries, timeout, and extend(...)

const client = createHttpClient({
  retry: {
    attempts: 3,
    baseDelayMs: 200,
  },
  timeout: 3000,
}).extend({
  baseURL: "https://api.myapp.com",
  headers: {
    authorization: "Bearer token",
  },
});

client.use(async (context, next) => {
  console.log("request", context.request.method, context.request.url);

  const response = await next();

  console.log("response", response.status);
  return response;
});

Prepared requests

Prepared requests are useful when you want keno to structure the request, but you want to decide later how to send it:

const prepared = client.prepare("GET", "/users/:id", {
  params: {
    id: "42",
  },
  query: {
    include: ["teams", "permissions"],
  },
});

const request = prepared.toRequest();
const raw = await prepared.fetch();
const response = await prepared.response();

console.log(request.url, raw.status, response.ok);

The fluent builder exposes the same preparation flow:

const prepared = client.get("/users/:id", {
  params: {
    id: "42",
  },
}).prepare();

Typed route contracts

Server and client code can share lightweight route definitions:

import {
  createHttpClient,
  defineHttpEndpoint,
  defineHttpRoute,
  defineHttpRoutes,
  type HttpClientSchemaFromRoutes,
} from "keno/client";

type CreateUser = {
  name: string;
};

type User = {
  id: string;
  name: string;
};

const routes = defineHttpRoutes(
  defineHttpRoute("/users/:id", {
    GET: defineHttpEndpoint<User, never, never, { id: string }>(),
  }),
  defineHttpRoute("/users", {
    POST: defineHttpEndpoint<User, CreateUser>(),
  }),
);

type Api = HttpClientSchemaFromRoutes<typeof routes>;

const client = createHttpClient<Api>({
  baseURL: "https://api.myapp.com",
});

const user = await client.GET("/users/:id", {
  params: {
    id: "42",
  },
});

const created = await client.POST("/users", {
  body: {
    name: "Ana",
  },
});

Transport And Concurrency

keno can stay minimal for a single-process app, but it also includes higher-level transport and concurrency primitives when needed.

Secure HTTP/2

import keno from "keno";
import { temporaryTls } from "keno/certificates";

const app = keno();
const tls = await temporaryTls({
  commonName: "127.0.0.1",
  hosts: ["127.0.0.1"],
});

await app.listen({
  host: "127.0.0.1",
  port: 3000,
  transport: "http2",
  allowHTTP1: true,
  tls,
}).ready();

Worker pool endpoints

import { createApp } from "keno/application";
import { createWorkerPool, threaded } from "keno/worker-pool";

const app = createApp();
const pool = createWorkerPool<{ value: number }, { result: number }>({
  entry: new URL("./worker.ts", import.meta.url),
  execArgv: ["--import", "tsx/esm"],
  size: 4,
});

app.get(
  "/cpu/:value",
  threaded(pool, {
    input: (request) => ({
      value: Number(request.params.value),
    }),
  }),
);

Other transport and concurrency features:

  • listenMany(...) for one app behind multiple listeners
  • keno/multi-server exports for explicit multi-listener orchestration
  • createThreadCluster(...) in keno/thread-cluster
  • threaded: true listener support with reusePort
  • temporary TLS helpers in keno/certificates

Examples

The repository ships with runnable examples you can start directly from the project root.

Core HTTP:

  • npm run example:basic
  • npm run example:client-http
  • npm run example:crud
  • npm run example:hosts
  • npm run example:modular-imports
  • npm run example:multi-server
  • npm run example:content-negotiation
  • npm run example:webhook-text

Files and static delivery:

  • npm run example:static-site
  • npm run example:download-center

WebSocket and realtime:

  • npm run example:client-websocket
  • npm run example:websocket
  • npm run example:realtime-chat
  • npm run example:http2

Concurrency:

  • npm run example:thread-cluster
  • npm run example:threaded-endpoints

If one of the default ports is already in use, override it per run:

PORT=3100 npm run example:basic

More details live in examples/README.md.

Benchmarks And WebSocket Conformance

keno includes an in-repo benchmark harness and Autobahn runner.

Run the benchmark suite:

npm run bench

For a shorter pass:

npm run bench:quick

Run the Autobahn WebSocket conformance harness:

npm run autobahn

The benchmark harness currently covers:

  • HTTP hello-world throughput
  • CPU-bound route throughput
  • static file throughput
  • WebSocket echo throughput
  • compressed WebSocket echo throughput

Current comparison targets include raw Node.js plus frameworks such as express, fastify, koa, tinyhttp, hono, ws, @fastify/websocket, and websocket.

Latest stored snapshot:

  • Source: bench/results/latest.json
  • Generated at: 2026-04-03T22:21:35.129Z
  • Profile: duration=5s, connections=50, pipelining=1, wsClients=20, wsMessages=500, cpuValue=28

Benchmark numbers are environment-dependent, so treat them as a reproducible local snapshot rather than a universal ranking. The Vs keno column uses the plain keno server in the same benchmark group as the baseline.

HTTP Hello-World

RuntimeReq/sLatency (ms)Vs keno
keno15325.603.041x
keno-plugin-stack11165.604.080.73x
keno-threaded39558.670.792.58x
node17083.202.271.11x
express101964.340.67x
fastify14831.203.010.97x
koa117003.780.76x
tinyhttp12285.603.600.80x
hono14005.603.060.91x

CPU-Bound Route

RuntimeReq/sLatency (ms)Vs keno
keno290175.221x
keno-worker-pool1042.3447.283.59x
keno-threaded1011.5048.733.49x
node285180.060.98x
express290177.211x
fastify290175.591x
koa285178.350.98x
tinyhttp290173.331x
hono290172.651x

Static File

RuntimeReq/sLatency (ms)Vs keno
keno13074.673.171x
keno-threaded26235.201.392.01x
node7489.206.200.57x
express7130.806.470.55x
fastify74946.150.57x
koa5009.209.490.38x
tinyhttp7120.406.480.54x

WebSocket Echo

RuntimeMsgs/sDuration (ms)Vs keno
keno26282.55380.481x
keno-rooms27957.90357.681.06x
keno-threaded47797.39209.221.82x
ws34734.30287.901.32x
fastify-websocket34821.78287.181.32x
websocket28844.27346.691.10x

WebSocket Compressed Echo

RuntimeMsgs/sDuration (ms)Vs keno
keno7475.161337.761x
keno-rooms7809.261280.531.04x
keno-threaded11070.89903.271.48x
ws9938.651006.171.33x
fastify-websocket11629.25859.901.56x

More details live in bench/README.md.

Validation

The repository is validated with:

  • npm run typecheck
  • npm test
  • npm run build

For the full local gate:

npm run check

Keywords

bun

FAQs

Package last updated on 03 May 2026

Related posts