
Security News
Lovable’s OJ Rewrites Vite’s Dev Server in Rust as AI Lowers the Cost of Forking Open Source
Lovable’s OJ rewrites Vite’s dev server in Rust, reducing memory use and preview times as AI lowers the cost of open source reimplementation.
A modular TypeScript-first server toolkit with typed HTTP routing, WebSocket support, and scalable transport primitives.
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.
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:
keno aims to keep one mental model across those layers instead:
app.use(...), app.get(...), and app.ws(...)permessage-deflateWebSocket implementationhost(...) and domain(...)listenMany(...) and keno/multi-serverreusePorthttp, https, and http2 transports, including RFC 8441 extended CONNECT for WebSockets over HTTP/2keno/client, keno/middleware, and keno/worker-poolnpm install keno
Runtime notes:
>=18.17.0globalThis.WebSocket, or with an injected constructorimport 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();
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);
keno is centered on a small set of runtime building blocks:
keno() and createApp()
KenoRouter
KenoRequest
KenoResponse
KenoWebSocket
HttpClient
KenoWebSocketClient
This keeps the everyday API small while still leaving room for host-aware routing, secure transports, concurrency, and shared contracts.
keno keeps the public surface modular through folder-based subpath exports:
keno, keno/application, keno/router, keno/request, keno/response, keno/typeskeno/middleware, keno/pluginskeno/client, keno/websocketkeno/certificates, keno/multi-server, keno/thread-cluster, keno/worker-poolExample:
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());
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 helpers include:
request.params, request.query, request.cookies, and request.bodyrequest.get(...), request.header(...), request.cookie(...), and request.param(...)request.accepts(...), request.acceptsLanguages(...), request.acceptsEncodings(...), and request.acceptsCharsets(...)request.is(...), request.xhr, and request.hasBodyrequest.origin, request.host, request.hostname, request.ip, request.secure, and request.transportawait 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(...)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 paramspermessage-deflate supportcreateWebSocketRooms() for room membership and broadcastcreateWebSocketHeartbeat() for keepalive managementcreateWebSocketClient() in keno/clienthttp, https, and http2If 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,
});
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:
heartbeatPluginopenApiPluginrequestLoggerPluginCustom plugins can be defined with definePlugin(...).
The HTTP client is designed to feel modern without getting in the way.
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 }>();
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 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();
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",
},
});
keno can stay minimal for a single-process app, but it also includes higher-level transport and concurrency primitives when needed.
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();
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 listenerskeno/multi-server exports for explicit multi-listener orchestrationcreateThreadCluster(...) in keno/thread-clusterthreaded: true listener support with reusePortkeno/certificatesThe repository ships with runnable examples you can start directly from the project root.
Core HTTP:
npm run example:basicnpm run example:client-httpnpm run example:crudnpm run example:hostsnpm run example:modular-importsnpm run example:multi-servernpm run example:content-negotiationnpm run example:webhook-textFiles and static delivery:
npm run example:static-sitenpm run example:download-centerWebSocket and realtime:
npm run example:client-websocketnpm run example:websocketnpm run example:realtime-chatnpm run example:http2Concurrency:
npm run example:thread-clusternpm run example:threaded-endpointsIf 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.
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:
Current comparison targets include raw Node.js plus frameworks such as express, fastify, koa, tinyhttp, hono, ws, @fastify/websocket, and websocket.
Latest stored snapshot:
bench/results/latest.json2026-04-03T22:21:35.129Zduration=5s, connections=50, pipelining=1, wsClients=20, wsMessages=500, cpuValue=28Benchmark 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.
| Runtime | Req/s | Latency (ms) | Vs keno |
|---|---|---|---|
| keno | 15325.60 | 3.04 | 1x |
| keno-plugin-stack | 11165.60 | 4.08 | 0.73x |
| keno-threaded | 39558.67 | 0.79 | 2.58x |
| node | 17083.20 | 2.27 | 1.11x |
| express | 10196 | 4.34 | 0.67x |
| fastify | 14831.20 | 3.01 | 0.97x |
| koa | 11700 | 3.78 | 0.76x |
| tinyhttp | 12285.60 | 3.60 | 0.80x |
| hono | 14005.60 | 3.06 | 0.91x |
| Runtime | Req/s | Latency (ms) | Vs keno |
|---|---|---|---|
| keno | 290 | 175.22 | 1x |
| keno-worker-pool | 1042.34 | 47.28 | 3.59x |
| keno-threaded | 1011.50 | 48.73 | 3.49x |
| node | 285 | 180.06 | 0.98x |
| express | 290 | 177.21 | 1x |
| fastify | 290 | 175.59 | 1x |
| koa | 285 | 178.35 | 0.98x |
| tinyhttp | 290 | 173.33 | 1x |
| hono | 290 | 172.65 | 1x |
| Runtime | Req/s | Latency (ms) | Vs keno |
|---|---|---|---|
| keno | 13074.67 | 3.17 | 1x |
| keno-threaded | 26235.20 | 1.39 | 2.01x |
| node | 7489.20 | 6.20 | 0.57x |
| express | 7130.80 | 6.47 | 0.55x |
| fastify | 7494 | 6.15 | 0.57x |
| koa | 5009.20 | 9.49 | 0.38x |
| tinyhttp | 7120.40 | 6.48 | 0.54x |
| Runtime | Msgs/s | Duration (ms) | Vs keno |
|---|---|---|---|
| keno | 26282.55 | 380.48 | 1x |
| keno-rooms | 27957.90 | 357.68 | 1.06x |
| keno-threaded | 47797.39 | 209.22 | 1.82x |
| ws | 34734.30 | 287.90 | 1.32x |
| fastify-websocket | 34821.78 | 287.18 | 1.32x |
| websocket | 28844.27 | 346.69 | 1.10x |
| Runtime | Msgs/s | Duration (ms) | Vs keno |
|---|---|---|---|
| keno | 7475.16 | 1337.76 | 1x |
| keno-rooms | 7809.26 | 1280.53 | 1.04x |
| keno-threaded | 11070.89 | 903.27 | 1.48x |
| ws | 9938.65 | 1006.17 | 1.33x |
| fastify-websocket | 11629.25 | 859.90 | 1.56x |
More details live in bench/README.md.
The repository is validated with:
npm run typechecknpm testnpm run buildFor the full local gate:
npm run check
FAQs
A modular TypeScript-first server toolkit with typed HTTP routing, WebSocket support, and scalable transport primitives.
The npm package kenojs receives a total of 1 weekly downloads. As such, kenojs popularity was classified as not popular.
We found that kenojs demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Lovable’s OJ rewrites Vite’s dev server in Rust, reducing memory use and preview times as AI lowers the cost of open source reimplementation.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.