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

@kozojs/core

v0.7.1

Published

TypeScript backend framework where routes, validation, OpenAPI, clients and tests share one contract — at native uWebSockets speed.

Readme

@kozojs/core

Build a TypeScript backend from one route contract.

Define a route once with a Zod schema — runtime validation, an OpenAPI 3.1 spec, a generated TypeScript client, and route-derived tests all come from that one definition. With no extra wiring you get:

  • Runtime validation — RFC 7807 errors on bad input, zero boilerplate
  • OpenAPI 3.1 — generated from the same schema
  • A route-derived client SDK — app.generateClient() → api.users.post({ body })
  • A static testing contract — createContractTestClient(app) derives paths, inputs, statuses, and outputs
  • Native-speed routing — an optional uWebSockets.js transport registers routes straight into a C++ radix trie

Kozo requires Node.js 20.19 or newer. app.listen() is the default Node.js transport; app.nativeListen() and app.listenSsr() add optional native and Vite integrations. app.fetch exposes the underlying Fetch API handler for compatible adapters.

npm install @kozojs/core zod

Quick Start

import { createKozo, z } from '@kozojs/core';

const app = createKozo();

app.get('/users/:id', {
  params: z.object({ id: z.string().uuid() }),
  response: z.object({ id: z.string(), name: z.string() }),
}, (ctx) => ({
  id: ctx.params.id,
  name: 'John Doe',
}));

await app.listen(3000);

Table of Contents


Server Modes

Kozo offers three server transports. Same routes, same handlers — pick the transport that fits your deployment.

app.listen(port?) — Node.js HTTP

Standard node:http server via @hono/node-server. Works everywhere.

await app.listen(3000);

app.nativeListen(port?) — uWebSockets.js (C++ transport)

Routes are registered directly with uWS's C++ radix trie router — zero JS routing overhead per request. Requires uWebSockets.js as a peer dependency (it is published on GitHub, not npm):

pnpm add uNetworking/uWebSockets.js#v20.66.0
const { port, server } = await app.nativeListen(3000);
// or with CORS:
await app.nativeListen({ port: 3000, cors: { origin: '*' } });

app.listenSsr(port, config) — Unified API + SSR

Single server for both API routes and Vite-powered SSR pages. See SSR Integration.

await app.listenSsr(3000, {
  root: './web',
  entryServer: 'src/entry-server.tsx',
});

Route Registration

Register routes with .get(), .post(), .put(), .patch(), .delete(). Each accepts an optional schema object for validation.

// No schema — handler only
app.get('/health', () => ({ status: 'ok' }));

// With schema — body, query, params, headers, response
app.post('/users', {
  body: z.object({ name: z.string(), email: z.string().email() }),
  response: z.object({ id: z.string().uuid(), name: z.string() }),
}, (ctx) => {
  return { id: uuid(), name: ctx.body.name };
});

The handler context ctx contains:

| Property | Type | Description | |----------|------|-------------| | ctx.body | Inferred from schema.body | Validated request body (POST/PUT/PATCH) | | ctx.query | Inferred from schema.query | Validated query parameters | | ctx.params | Inferred from schema.params | Validated path parameters | | ctx.headers | Inferred from schema.headers | Validated request headers | | ctx.services | TServices | Injected services | | ctx.json(data, status?) | Response | Return JSON response | | ctx.text(data, status?) | Response | Return text response | | ctx.html(data, status?) | Response | Return HTML response | | ctx.req | KozoRequest | Typed request helper |

Handlers can return a plain object (auto-serialized as JSON) or a Response object for full control.


Static Route Contracts

TypeScript cannot derive compile-time types from routes discovered only at runtime. Use createRouter() and mount() when a consumer such as @kozojs/testing needs the complete route tree:

import { createKozo, createRouter, z } from '@kozojs/core';

const users = createRouter()
  .get('/:id', {
    params: z.object({ id: z.string() }),
    response: {
      200: z.object({ id: z.string(), name: z.string() }),
      404: z.object({ message: z.string() }),
    },
  }, ({ params, json }) => {
    return params.id !== 'missing'
      ? json({ id: params.id, name: 'Ada' }, 200)
      : json({ message: 'User not found' }, 404);
  });

const app = createKozo().mount('/users', users);

Fluent chaining directly from createKozo() also preserves route types. Calling app.get(...) and ignoring its returned value still registers the runtime route, but it cannot change the already inferred type of app. Dynamically loaded file-system and plugin routes remain accessible through runtime introspection and raw test clients unless the application exports an explicit static contract for them.


Route Groups

Group routes under a common prefix:

app.group('/api/v1', (r) => {
  r.get('/users', (ctx) => listUsers());
  r.get('/users/:id', { params: uuidParams }, (ctx) => getUser(ctx.params.id));
  r.post('/users', { body: CreateUserSchema }, (ctx) => createUser(ctx.body));
});
// Registers: GET /api/v1/users, GET /api/v1/users/:id, POST /api/v1/users

Schema Validation

Kozo uses Zod natively — no AJV, no JSON Schema intermediate step. Schemas are compiled once at route registration.

app.post('/users', {
  body: z.object({
    email: z.string().email(),
    name: z.string().min(2).max(50),
    age: z.number().min(18),
  }),
  query: z.object({
    dryRun: z.coerce.boolean().optional(),
  }),
  params: z.object({
    orgId: z.string().uuid(),
  }),
  response: UserSchema,         // bare schema -> normalized to { 200: UserSchema }
  // or: response: { 200: UserSchema, 201: CreatedSchema }
}, handler);

Invalid requests return RFC 7807 application/problem+json:

{
  "type": "https://kozo-docs.vercel.app/docs/core/errors#validation-failed",
  "title": "Validation Failed",
  "status": 400,
  "errors": [
    { "field": "email", "message": "Invalid email", "code": "invalid_string" }
  ]
}

Response schema = contract (read this)

When you set response on a route, Kozo treats it as the public API contract, not just typing sugar:

  • Compiled serialization — if the schema maps to JSON Schema (z.object, primitives, arrays, …), Kozo compiles a fast-json-stringify serializer once at route registration. Schemas with .transform(), z.date(), or z.any() use JSON.stringify instead (safe fallback — no silent wrong compile).
  • Undeclared fields are dropped — extra properties on the handler return value (e.g. DB columns not in the schema) are omitted from the JSON response. This is intentional contract enforcement: declare everything you return, or remove response from the route if you want pass-through serialization.
  • Status maps are exact — ctx.json(data, 201) uses the schema declared for status 201. An undeclared status uses normal JSON serialization for backward compatibility, so declare every public status when field stripping must be enforced.
app.get('/users/:id', {
  params: z.object({ id: z.string() }),
  response: z.object({ id: z.string(), name: z.string() }), // only these two keys in JSON
}, ({ params, services }) => services.db.findUser(params.id));
// If the DB row has `{ id, name, email, createdAt }`, the client receives `{ id, name }` only.

Services (Dependency Injection)

Pass typed services at construction — every handler receives them via ctx.services:

interface AppServices {
  db: Database;
  cache: RedisClient;
  stripe: Stripe;
}

const app = createKozo<AppServices>({
  services: { db, cache, stripe },
});

app.get('/users', (ctx) => {
  // ctx.services.db is fully typed as Database
  return ctx.services.db.users.findMany();
});

Guards (security — single source of truth)

app.guard(pattern, fn) registers a transport-agnostic check: it runs as Hono middleware under listen() and compiled into the uWS fast path under nativeListen() — identical semantics, native speed. Use guards for auth, roles, and rate limits.

Register a guard before the routes it protects. This preserves middleware order on the Node/Hono transport and matches the native transport.

import { rateLimitGuard, requireSecret } from '@kozojs/core';
import { jwtGuard, roleGuard } from '@kozojs/auth';

app.guard('/api/*', jwtGuard(requireSecret('JWT_SECRET'), { publicPaths: ['/api/health'] }));
app.guard('/api/admin/*', roleGuard('admin'));
app.guard('/api/auth/*', rateLimitGuard({ max: 20, window: 60 }));

// Custom guard: allow (return nothing), attach user, or deny
app.guard('/internal/*', (req) => {
  if (req.header('x-api-key') !== process.env.API_KEY) {
    return { deny: { status: 401, body: { error: 'invalid api key' } } };
  }
});

CORS is handled at the transport level (preflight included):

await app.nativeListen({ port: 3000, cors: { origin: ['https://app.com'], credentials: true } });

Middleware

Register Hono middleware globally or per-path:

import { logger, errorHandler } from '@kozojs/core/middleware';

app.middleware(logger());        // request logging
app.middleware(errorHandler());  // error handler (RFC 7807)

// Custom middleware (needs the Hono Context)
app.middleware('/admin/*', async (c, next) => {
  c.set('requestId', crypto.randomUUID());
  return next();
});

Under nativeListen(), routes covered by middleware patterns are served through the Hono bridge to guarantee correctness (~35% slower than the native path). Prefer app.guard() for security checks — it stays native. Versions ≤ 0.5.15 silently bypassed middleware under nativeListen(); upgrade to ≥ 0.5.16.

Native transport limits (multipart, streaming/SSE on bridged routes, HTTPS): see Common Pitfalls §12.

Built-in Middleware

| Middleware | Import | Options | |-----------|--------|---------| | logger(options?) | @kozojs/core/middleware | prefix?: string, colorize?: boolean | | cors(options?) | @kozojs/core/middleware | origin, allowMethods, allowHeaders, maxAge, credentials | | rateLimit(options) | @kozojs/core/middleware | max, window (seconds), keyGenerator?, store? (Redis etc.) | | errorHandler() | @kozojs/core/middleware | Catches KozoError -> RFC 7807 response |


Error Handling (RFC 7807)

All errors follow RFC 7807 Problem Details. Throw any KozoError subclass and it becomes a structured response.

import {
  KozoError,
  NotFoundError,
  BadRequestError,
  UnauthorizedError,
  ForbiddenError,
  ConflictError,
  GoneError,
  ValidationFailedError,
} from '@kozojs/core';

app.get('/users/:id', { params: uuidParams }, (ctx) => {
  const user = db.users.find(ctx.params.id);
  if (!user) throw new NotFoundError();        // -> 404
  if (!canAccess(user)) throw new ForbiddenError('Insufficient permissions'); // -> 403
  return user;
});

Error classes:

| Class | Status | Default Message | |-------|--------|-----------------| | KozoError | any | (custom) | | BadRequestError | 400 | "Bad Request" | | UnauthorizedError | 401 | "Unauthorized" | | ForbiddenError | 403 | "Forbidden" | | NotFoundError | 404 | "Resource Not Found" | | ConflictError | 409 | "Conflict" | | GoneError | 410 | "Gone" | | ValidationFailedError | 400 | (custom, includes .errors array) |

Pre-built response helpers (zero-allocation hot path):

import {
  notFoundResponse,
  unauthorizedResponse,
  forbiddenResponse,
  internalErrorResponse,
  validationErrorResponse,
} from '@kozojs/core';

Graceful Shutdown

Kozo drains in-flight requests before closing. No request is dropped mid-flight.

await app.listen(3000);

// Later (e.g. on SIGTERM):
await app.shutdown({
  timeoutMs: 30000,
  onShutdownStart: (inflight) => console.log('Draining ' + inflight + ' requests'),
  onShutdownComplete: () => console.log('Clean exit'),
});

Full lifecycle control via ShutdownManager:

const manager = app.getShutdownManager();

// Register database cleanup
manager.setDatabase(db, 'postgresql'); // also: 'mysql', 'sqlite'

// Custom cleanup hooks (run after draining, before DB close)
manager.addCleanupHook(async () => {
  await cache.quit();
  await queue.close();
});

// Wire to process signals
process.on('SIGTERM', () => app.shutdown());
process.on('SIGINT', () => app.shutdown());

During shutdown:

  1. New requests -> 503 Service Unavailable
  2. In-flight requests -> allowed to complete (up to timeoutMs)
  3. Cleanup hooks run
  4. Database connections closed
  5. Server closed

Type-Safe Client Generation

Generate a fully typed TypeScript client from your routes:

const code = app.generateClient({
  baseUrl: 'https://api.example.com',
  includeValidation: true,    // embed Zod schemas for client-side validation
  validateByDefault: false,   // opt-in per request
});

writeFileSync('./client/api.ts', code);

Generated route-tree usage:

import { createKozoClient } from './client/api';

const api = createKozoClient({ baseUrl: 'https://api.example.com' });

const users = await api.users.get({
  query: { page: 1 },
});

const created = await api.users.post({
  body: { name: 'Jane', email: '[email protected]' },
});

if (created.status === 201) {
  created.body.id; // string
}

const detail = await api.users.$id.get({
  params: { id: created.status === 201 ? created.body.id : 'missing' },
});

if (detail.status === 404) {
  detail.body.message; // narrowed to the declared 404 schema
}

createKozoClient() is the preferred API. It returns declared HTTP statuses as the union { status, headers, body, ok }; an undeclared status throws KozoUnexpectedResponseError. The generated KozoClient class retains flat methods such as usersById() as deprecated compatibility aliases. Those legacy methods preserve their historical behavior and throw KozoApiError for every non-2xx response.

Options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | baseUrl | string | '' | API base URL | | includeValidation | boolean | true | Include Zod schemas in output | | validateByDefault | boolean | false | Enable validation in client constructor | | defaultHeaders | Record&lt;string, string&gt; | {} | Default request headers |


OpenAPI Generation

Mount Swagger UI and an OpenAPI 3.1 document from registered routes:

app.mountDocs({
  title: 'My API',
  version: '1.0.0',
  servers: [{ url: 'https://api.example.com' }],
});
  • Swagger UI: /docs
  • OpenAPI JSON: /docs.json

Outside production the routes are enabled by default. In production, opt in explicitly:

app.mountDocs({
  title: 'My API',
  version: '1.0.0',
  enabled: process.env.ENABLE_API_DOCS === 'true',
});

For custom spec pipelines, use createOpenAPIGenerator() directly. generateSwaggerHtml(specUrl, title?) accepts the URL of an OpenAPI document, not the document object.

Schemas are converted via Zod v4 native z.toJSONSchema(). Supported route metadata includes path params, query params, request bodies, response schemas, tags, Bearer auth, and summaries.


File-System Routing

Auto-register routes from the file system:

const app = createKozo({ routesDir: './src/routes' });
await app.loadRoutes();

Convention:

| File | Route | |------|-------| | routes/users.ts | GET/POST /users | | routes/users/[id].ts | GET/PUT/DELETE /users/:id | | routes/_middleware.ts | Skipped (prefixed with _) | | routes/users.test.ts | Skipped (test file) |

Each route file exports a default handler and optional schema/meta:

// routes/users/[id].ts
import { z } from 'zod';

export const schema = {
  params: z.object({ id: z.string().uuid() }),
  response: UserSchema,
};

export const meta = { auth: true, tags: ['users'] };

export default (ctx) => {
  return ctx.services.db.users.find(ctx.params.id);
};

Programmatic API:

import { createFileSystemRouting, applyFileSystemRouting } from '@kozojs/core/middleware';

WebSocket (uWS)

WebSocket support via uWebSockets.js native pub/sub. Requires nativeListen().

app.ws('/ws/chat', {
  open(ws) {
    ws.subscribe('chat');
  },
  message(ws, data) {
    ws.publish('chat', data);
  },
  close(ws) {
    console.log('disconnected');
  },
});

await app.nativeListen(3000);

With typed user data and auth upgrade:

app.ws<{ userId: string }>('/ws/secure', {
  upgrade(req) {
    const userId = verifyToken(req.headers['authorization']);
    if (!userId) return false; // reject upgrade
    return { userId };         // attached as ws.data
  },
  open(ws) {
    console.log(ws.data.userId + ' connected');
    ws.subscribe('user:' + ws.data.userId);
  },
  message(ws, data) {
    ws.publish('user:' + ws.data.userId, data);
  },
});

Note: app.listen() will warn if WebSocket routes are registered — use app.nativeListen() instead.


SSR Integration

Unified API + Vite SSR from a single server. No separate frontend server or proxy.

import path from 'node:path';
import { createKozo } from '@kozojs/core';

const app = createKozo({ routesDir: './src/routes' });
await app.loadRoutes();

await app.listenSsr(3000, {
  root: path.resolve('./web'),
  entryServer: 'src/entry-server.tsx',
  apiPrefix: '/api',
});

How it works: requests matching apiPrefix go to Hono, everything else -> Vite SSR pipeline.

  • Dev mode: Vite middleware for HMR + optional SSR rendering (auto-detected)
  • Prod mode: Static files from dist/client/ + pre-built SSR from dist/server/

Supports React 18 streaming (renderToPipeableStream) and string rendering.

SSR Config

| Option | Type | Default | Description | |--------|------|---------|-------------| | root | string | — | Web app root (where index.html lives) | | entryServer | string | — | Server entry relative to root | | apiPrefix | string \| string[] | '/api' | Routes that bypass SSR | | devSsr | boolean | auto-detected | Enable SSR in dev mode | | template | string | 'index.html' | HTML template path | | appPlaceholder | string | &lt;!--app-html--&gt; (default) | Placeholder for rendered HTML | | headPlaceholder | string | &lt;!--ssr-head--&gt; (default) | Placeholder for head tags | | distClient | string | 'dist/client' | Built client assets | | distServer | string | 'dist/server' | Server bundle directory |


Helper Schemas & Utilities

Common schemas to avoid repeating boilerplate:

import {
  paginationSchema,  // { page: z.coerce.number().default(1), limit: ... }
  uuidParams,        // { id: z.string().uuid() }
  idParams,          // { id: z.coerce.number().int().positive() }
  timestamps,        // { createdAt: z.date(), updatedAt: z.date() }
  sortSchema,        // { sortBy?: string, sortOrder: 'asc' | 'desc' }
  searchSchema,      // { q?: string }
  successSchema,     // { success: boolean, message?: string }
  deletedSchema,     // { success: boolean, deletedId: string }
  uuid,              // () => string (crypto.randomUUID)
  paginate,          // (items, page, limit) -> PaginatedResult
  defineEnv,         // validate process.env with Zod at startup
} from '@kozojs/core';

Environment validation:

const env = defineEnv({
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
});
// Throws at startup with clear message if any variable is missing/invalid

Pagination:

app.get('/users', { query: paginationSchema }, (ctx) => {
  return paginate(allUsers, ctx.query.page, ctx.query.limit);
  // -> { data: [...], total, page, limit, totalPages, hasNext, hasPrev }
});

Fast Response Utilities

Zero-allocation response helpers for custom native handlers:

import {
  fastWriteJson,       // 200 JSON
  fastWriteText,       // 200 text/plain
  fastWriteHtml,       // 200 text/html
  fastWriteJsonStatus, // JSON with custom status
  fastWrite404,        // pre-built 404
  fastWrite500,        // pre-built 500
  fastWrite400,        // validation error
  fastWriteError,      // KozoError -> problem+json
} from '@kozojs/core';

Runtime Compatibility

// Standard Node.js HTTP
await app.listen(3000);

// Node.js + optional uWebSockets.js dependency
await app.nativeListen(3000);

// Fetch API integration; confirm compatibility in your target adapter
export default { fetch: app.fetch };

API Reference

createKozo&lt;TServices&gt;(config?)

Create a Kozo application.

| Option | Type | Default | Description | |--------|------|---------|-------------| | services | TServices | {} | Dependency injection container | | routesDir | string | — | Directory for file-system routing | | maxBodyBytes | number | 1048576 | Max request body size — larger requests get a 413 | | logger | boolean | true | Set false to silence the startup banner (tests, benchmarks) |

Instance Methods

| Method | Description | |--------|-------------| | .get(path, schema?, handler) | Register a GET route | | .post(path, schema?, handler) | Register a POST route | | .put(path, schema?, handler) | Register a PUT route | | .patch(path, schema?, handler) | Register a PATCH route | | .delete(path, schema?, handler) | Register a DELETE route | | .group(prefix, fn) | Group routes under a prefix | | .ws(path, handler) | Register a WebSocket route (requires nativeListen) | | .middleware(path?, handler) | Register Hono middleware | | .use(plugin) | Install a plugin | | .listen(port?) | Start Node.js HTTP server (default: 3000) | | .nativeListen(port?) | Start uWebSockets.js server | | .listenSsr(port, config) | Start unified API + SSR server | | .loadRoutes(dir?) | Load routes from file system | | .shutdown(options?) | Graceful shutdown | | .generateClient(options?) | Generate typed client SDK | | .mountDocs(options?) | Mount Swagger UI and an OpenAPI 3.1 document | | .getRoutes() | Inspect registered routes | | .getShutdownManager() | Access shutdown manager | | .getApp() | Access underlying Hono instance | | .fetch | Hono fetch handler (for Workers/Deno) |

Exports Map

import { ... } from '@kozojs/core';           // main exports
import { ... } from '@kozojs/core/middleware'; // middleware

License

MIT