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

@arki/http

v0.0.3

Published

Typed HTTP for ARKI — route contracts as data, zod-validated handlers, request-scope derivers, SSE streaming, OpenAPI generation, and the DOT http() plugin.

Readme

@arki/http

Typed HTTP for ARKI — route contracts as data, zod-validated handlers, request-scope derivers, SSE streaming, and deterministic OpenAPI generation. Ships a DOT adapter (@arki/http/dot) whose http() plugin turns a composed app into a running server with correct drain-on-shutdown semantics.

No decorators, no controller scanning, no runtime DI container. Contracts are plain values; wiring is checked at compile time.

The model

A route is split in two:

  • Contract — pure data at module scope: method + path + zod schemas + a stable id. Registerable in the DOT manifest (sync configure), renderable as OpenAPI without booting, and the source of the handler's inferred signature.
  • Binding — the handler, attached inside a plugin's boot where the plugin's typed needs are already injected. Handlers close over real services; no service locator exists.
import { route, routes } from '@arki/http';
import { z } from 'zod';

export const listOrders = route.get('/orders', {
  id: 'orders.list',
  query: z.object({ status: z.enum(['open', 'shipped']).optional() }),
  output: z.array(Order),
});

export const createOrder = route.post('/orders', {
  id: 'orders.create',
  body: CreateOrder,
  output: Order,
});

Input validation (query/body) happens before your handler runs; output is validated against the contract on the way out. Failures serialize as a coded envelope: { "error": { "code", "message", "remediation?", "docsUrl?" } }.

Inside a DOT app

import { plugin, provide, service, token } from '@arki/dot/plugin';
import { routes, type RouteBundle } from '@arki/http';

export const OrdersRoutes = token<RouteBundle>()('orders.routes');

export const ordersPlugin = plugin({
  name: 'orders',
  needs: { db: service<Db>() },
  actions: [listOrders, createOrder], // manifest + OpenAPI metadata
  async boot({ db }) {
    return provide(
      OrdersRoutes,
      routes()
        .bind(listOrders, async ({ query }) => db.orders.list(query))
        .bind(createOrder, async ({ body }, ctx) => db.orders.create(body, ctx.requestId)),
    );
  },
});

Mount the http() plugin last, naming the bundles it serves:

import { defineApp, hookSignals } from '@arki/dot';
import { http } from '@arki/http/dot';

const app = await defineApp('shop-api')
  .use(db({ url: DATABASE_URL }))
  .use(ordersPlugin)
  .use(http({ port: 3000, bundles: [OrdersRoutes] }))
  .start();

hookSignals(app);

http() derives its needs from the tokens you pass — forget to mount ordersPlugin and the .use(http(...)) line fails to typecheck. Because the plugin is declared last, reverse-order stop halts ingress FIRST: the server stops accepting, drains in-flight requests (drainTimeoutMs, default 10 s, ARKI_HTTP_E005 on overrun), and only then do feature plugins tear down. boot builds the router without listening; start listens; dispose severs remaining connections and releases the port.

The published httpServer service carries the composed fetch: (Request) => Promise<Response> — also the unit-test seam: call your whole app without a socket.

Request scope — derivers, not containers

Per-request values are typed context keys derived by plain functions. Derivers run in declaration order; each adds a key later handlers (and later derivers) see typed. A deriver that throws HttpError is the whole "guard" story:

import { derive, HttpError, HTTP_ERROR_CODES } from '@arki/http';

// inside boot — `auth` is an injected singleton
const withPrincipal = derive('principal', async req => {
  const p = await auth.verify(req.headers.get('authorization'));
  if (!p) throw new HttpError(401, HTTP_ERROR_CODES.unauthorized, 'missing or invalid credentials');
  return p;
});

routes()
  .derive(withPrincipal)
  .derive('db', (_req, ctx) => dbForTenant(ctx.principal.tenantId))
  .bind(listOrders, async (_input, ctx) => ctx.db.orders.list());

Request scope is function application over the compile-time-wired singleton graph — no per-request container resolution, no infectious scopes. For code deep in a call stack you don't control, the fenced-off @arki/http/context entry exposes requestContext() (AsyncLocalStorage). It is a last resort; prefer the explicit ctx.

App-wide middleware

For cross-cutting concerns that must see or touch the raw request/response — CORS, request logging — pass fetch-shaped middleware to http() (first = outermost). A middleware that throws HttpError short-circuits into the coded envelope:

http({ port: 3000, bundles: [OrdersRoutes], middleware: [requestLog(logger), cors(origins)] })

Middleware wraps the whole engine and is untyped by design; anything that produces a per-request value belongs in a deriver instead. Standalone users compose the same way with composeMiddleware(engine.fetch, [...]).

Streaming

Typed SSE. route.sse declares a text/event-stream contract; the binding is an async generator. Every yield is validated against the event schema and framed as a data: message; client disconnect aborts ctx.signal and runs the generator's finally:

const progress = route.sse('/orders/:id/progress', {
  id: 'orders.progress',
  event: z.discriminatedUnion('type', [Queued, Shipped]),
  heartbeatMs: 15_000, // optional :keepalive comments
});

routes().bind(progress, async function* ({ params }, ctx) {
  for await (const step of tracker.watch(params.id, ctx.signal)) {
    yield step;
  }
});

Raw streams. Any handler may return a Response wrapping a ReadableStream — file downloads, AI token streams. The engine is fetch-native end to end.

Mounting foreign handlers

An oRPC router, a tRPC fetch adapter, or any fetch-shaped handler mounts as one manifest route — the migration on-ramp for existing backends:

routes().mount('/rpc', rpcHandler, { id: 'rpc', transport: 'orpc' });

OpenAPI

import { toOpenApi } from '@arki/http';

const document = toOpenApi([listOrders, createOrder, progress], {
  title: 'shop-api',
  version: '1.0.0',
});

Deterministic — same contracts, byte-identical output. Inside a DOT app, contract actions put the JSON Schemas into the manifest, so dot explain --as openapi renders the document from the static manifest without booting anything.

Without DOT

The core is framework-agnostic:

import { buildEngine, listen, routes } from '@arki/http';

const engine = buildEngine([bundle]);
const handle = await listen(engine.fetch, { port: 3000 });

listen uses Bun.serve under Bun and @hono/node-server under Node. Routing is Hono internally; no Hono type appears in the public API.

Error codes

Stable API — match on codes, never parse messages.

| Code | Meaning | | --- | --- | | ARKI_HTTP_E001 | start could not bind the port | | ARKI_HTTP_E002 | duplicate route: two bindings claim the same method+path (or a bundle token is listed twice) | | ARKI_HTTP_E005 | stop exceeded drainTimeoutMs with requests in flight | | ARKI_HTTP_E006 | malformed mount path | | ARKI_HTTP_E007 | deriver key duplicates an earlier deriver or a base context key | | ARKI_HTTP_E008 | a bundle token's service was missing at boot (erased composition only) | | ARKI_HTTP_E400 | request validation failed (query/body) or body is not valid JSON | | ARKI_HTTP_E401 / E403 | credential / permission rejection (thrown by your derivers) | | ARKI_HTTP_E404 | no route matches | | ARKI_HTTP_E500 | handler crash or output-schema violation |

E0NN codes surface through the DOT lifecycle into app.diagnostics; E4xx/E5xx codes surface on the wire in the error envelope. Unexpected error messages are redacted when NODE_ENV=production.

License

MIT