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

s200

v0.3.0

Published

Data + functions server framework on the Web Standard: koa-style onion middleware, hono-style multi-runtime portability, replaceable modules, fully tree-shakable. Zero dependencies at the core (s200/events adds @for-fun/event-emitter).

Readme

s200

A data + functions server framework for the Web Standard. Koa-style onion middleware, hono-style multi-runtime portability, and a fully tree-shakable, replaceable module surface. Zero dependencies at the core — every battery too, except s200/events, which builds on @for-fun/event-emitter.

import { createApp, get, json, post, readJson, setHeader, use } from 's200';
import { serve } from 's200/node';

const app = createApp();

use(app, async (ctx, next) => {
  await next();
  setHeader(ctx, 'x-powered-by', 's200');
});

get(app, '/users/:id', (ctx) => json(ctx, { id: ctx.params.id }));
post(app, '/echo', async (ctx) => json(ctx, await readJson(ctx)));

const server = await serve(app, { port: 3000 });
console.log(`listening on ${server.url}`);

Save it as app.ts and run it with node app.ts — Node ≥ 23.6 strips the types natively; on older Node use npx tsx app.ts or node --experimental-strip-types app.ts (≥ 22.6).

The paradigm: data + functions

An application is plain data — routes, middlewares, config. Behavior never hangs off it; every capability is a top-level function taking the app (or request context) as its first argument:

const app = createApp();       // data
use(app, logger);              // behavior: (app, middleware) => app
get(app, '/x', handler);       // behavior: (app, pattern, handler) => app
handle(app, request);          // behavior: (app, Request) => Promise<Response>

No class, no new App(), no app.get(...) method calls. The payoff is mechanical: bundlers can drop every capability you don't import (pnpm verify:tree-shaking proves the body/static modules vanish from a minimal bundle, and pnpm size gates the shipped sizes), and the core translates directly to struct + procedure shapes in any language.

Installation

Scaffold a project — npm create s200@latest walks you through it (runtime picker: node / bun / cloudflare):

npm create s200@latest

Or install the framework manually:

npm install s200
  • Node.js ≥ 20.3 (adapter: s200/node), Bun (adapter: s200/bun). Edge runtimes can consume the core directly — it only touches Web Standard Request/Response.
  • Running the TypeScript examples directly needs Node ≥ 23.6 (native type stripping), or ≥ 22.6 with NODE_OPTIONS=--experimental-strip-types.
  • s200 — the runtime-agnostic core
  • s200/node — node:http adapter (serve, createFileReader, createRealPathGuard, …). Memory: the default (Web Standard) path sits at ~99 MB steady-state RSS after a 5000-request warmup — level with hono (2026-10-26 benchmark update). serve(app, { light: true }) remains the escape hatch when you need the last ~10 MB and +20–40% throughput: it skips the platform Request/Response constructors entirely.
  • s200/bun — Bun.serve adapter (serve, createFileReader, createRealPathGuard, …)
  • s200/cors / s200/logger / s200/route-table / s200/query / s200/websocket / s200/etag / … — opt-in batteries (see Batteries); WebSockets bridge to node, Bun, and Cloudflare Workers (s200/websocket/cloudflare)

Documentation

The full API documentation lives on the docs site — wmzy.github.io/s200 — available in English and 简体中文:

Community

Bugs go to GitHub Issues — include the runtime and a minimal repro. Ideas, questions, and usage discussion belong in GitHub Discussions. Vulnerabilities are reported privately — see SECURITY.md. To contribute code, docs, or benchmarks, start with CONTRIBUTING.md.

Stability & versioning

s200 is heading to 1.0 with an explicit contract: the surface real apps touch — createApp options, the registrars, the Ctx guarantees (pre-decoded params, cached url, disconnect-aware signal, per-request state, always-materialized res), the respond helpers and their content-length contract, the error model (httpError/isHttpError/toErrorResponse + the throws gate), routing semantics (first registration wins, HEAD→GET, 405+Allow, strict trailing slashes), and a core set of battery entries (the three adapters, s200/test, s200/dev, and the everyday middleware: cors, logger, cookies, validate, query, rate-limit, compress, etag, request-id, timeout, websocket) — freezes at 1.0 and changes only in majors after that. The remaining entries stay stable and semantic-versioned, but their surface may adjust within minors (with migration notes) until they accrue real-world usage; the sharding layer is experimental. Releases are automated by semantic-release over conventional commits; before 1.0 the 0.x allowance applies (any minor may break, with migration notes), after 1.0 deprecations run the mark → migration guide → two minors rhythm. What is honestly not frozen yet: light-mode details and the codegen output format.

The full frozen-surface list and policy: docs/stability.md. Migration guides: from Express, from Koa, from Hono.

Development

pnpm build                 # vite lib build (es + cjs, every entry tree-shaken) + d.ts/d.mts emission
pnpm test                  # vitest single run (add --maxWorkers=4 to cap concurrency)
pnpm start                 # vitest watch
pnpm lint / lint:ci
pnpm check:paradigm        # enforces data + functions (no class/this/new/extends in src)
pnpm verify:tree-shaking   # asserts unused modules are shaken from a minimal bundle
pnpm smoke                 # runs scripts/smoke.mjs under node AND bun
pnpm bench                 # router dispatch micro-benchmark (ROUTES/ITERATIONS env)
pnpm bench:http            # whole-request throughput vs hono/express/fastify/elysia (isolated processes)
pnpm docs:dev / docs:build # vitepress documentation site (docs-site/)

s200 dev ./app.ts — the dev runner. Reloads swap the route table atomically on one server: in-flight requests finish on the old chain, new requests hit the fresh table — no restart, no port churn. The entry must export the app as default or as a named export app. The first load prints the route table (aligned METHOD PATTERN, registration order); reloads keep the one-line count. 404 route hints are on by default (--no-hints disables) — misses that overlap the table get a did you mean hint, and the user's app object is never mutated. --overlay (off by default) injects a zero-dependency script into byte-backed text/html responses and serves /__s200/dev-events (SSE) so the browser reloads on s200 dev reloads and shows a dismissible error box when a load fails; streamed HTML is skipped. On Node ≥ 22.15 the whole module graph re-evaluates per reload (edit any imported module, not just the entry); watch events fire only for source files, so an app writing logs or data files into its own directory can't feed reload loops. TypeScript entries need Node ≥ 22.18 (or ≥ 22.6 with --experimental-strip-types) — the runner fails fast with that remedy on older Nodes. Two one-shot subcommands share the same loader and TypeScript floor: s200 routes <entry> [--json] prints the route table (aligned, or the full JSON data), s200 codegen <entry> [--out file] [--name X] [--base-url U] prints a typed client module; unknown commands exit non-zero listing every command.

The docs site deploys automatically to GitHub Pages (/s200/) by .github/workflows/docs.yml on pushes to main that touch docs-site/, docs/, or the lockfile. The Pages base is set from GITHUB_ACTIONS in docs-site/.vitepress/config.mts, so local dev and preview stay rooted at /. Available in English and 简体中文 — docs/ holds the English source of truth and docs/zh/ the Chinese translation, included verbatim into docs-site/.

Runnable examples live in examples/ — rest-jwt (JWT-gated REST API + typed client), sse-dashboard (SSE ticker + static page), ws-chat (websocket rooms), upload-app (streaming multipart uploads with s200/upload), crud-openapi (annotated CRUD with OpenAPI docs + generated client — the data + functions story end to end): pnpm --filter @s200-example/rest-jwt smoke and friends.

See docs/benchmarks.md for benchmark numbers and methodology, docs/comparison.md for how s200 stacks up against the alternatives (Hono, Express, Fastify, Koa, Elysia), and the migration guides when coming from Express, Koa, or Hono.

License

MIT