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

@ontrails/http

v0.2.3

Published

Framework-agnostic HTTP route derivation and Web Fetch request handling for Trails. Pair this package with `@ontrails/hono` when you want Hono portability, or use `@ontrails/http/bun` when you want Bun-native serving without a third-party framework.

Downloads

1,008

Readme

@ontrails/http

Framework-agnostic HTTP route derivation and Web Fetch request handling for Trails. Pair this package with @ontrails/hono when you want Hono portability, or use @ontrails/http/bun when you want Bun-native serving without a third-party framework.

Usage

import { trail, topo, Result } from '@ontrails/core';
import { surface } from '@ontrails/hono';
import { z } from 'zod';

const greet = trail('greet', {
  input: z.object({ name: z.string().describe('Who to greet') }),
  output: z.object({ message: z.string() }),
  intent: 'read',
  implementation: (input) => Result.ok({ message: `Hello, ${input.name}!` }),
});

const graph = topo('myapp', { greet });
await surface(graph, { port: 3000 });

This starts a Hono-based HTTP server. The greet trail becomes GET /greet?name=... because its intent is 'read'.

For Bun-native HTTP without Hono, use the Bun-native HTTP binding subpath:

import { surface } from '@ontrails/http/bun';

await surface(graph, { port: 3000 });

@ontrails/http/bun uses Bun's native Bun.serve({ routes }) fast path and keeps the shared Web Fetch handler as the fallback. It requires Bun >=1.2.3 and does not add a third-party runtime dependency.

Rendering and runtime binding

The HTTP package follows the surface API naming split:

  • derive* exports are pure renderings from the topo. Use deriveHttpRoutes() for route definitions and deriveOpenApiSpec() for the OpenAPI contract.
  • create* exports build runtime objects without opening a network boundary. @ontrails/http/fetch exports createRouteHandler() for one route and createFetchHandler() for a full topo dispatcher.
  • surface() opens the runtime boundary. @ontrails/hono opens an adapter binding to Hono; @ontrails/http/bun opens the native Bun HTTP binding.

The shared @ontrails/http/fetch kernel owns query/body parsing, content-length validation, public error rendering, diagnostics, request IDs, headers, abort propagation, and webhook verification/parsing behavior. Hono and Bun both consume that kernel so route semantics stay aligned.

For more control, build the routes yourself:

import { deriveHttpRoutes } from '@ontrails/http';

const result = deriveHttpRoutes(graph);
if (result.isErr()) throw result.error; // ValidationError on route collision
for (const route of result.value) {
  console.log(`${route.method} ${route.path} → ${route.trailId}`);
}

deriveHttpRoutes returns Result<HttpRouteDefinition[], Error> rather than a bare array. It returns Result.err(ValidationError) if two trails derive the same (method, path) pair.

OpenAPI is the HTTP surface's persisted client contract rendering:

import { deriveOpenApiSpec } from '@ontrails/http';

const spec = deriveOpenApiSpec(graph, { basePath: '/api' });

deriveOpenApiSpec() emits an OpenAPI 3.1 document from the same trail contracts used by deriveHttpRoutes().

API

| Export | What it does | | --- | --- | | deriveHttpRoutes(graph, options?) | Build framework-agnostic route definitions from a topo | | deriveOpenApiSpec(graph, options?) | Generate an OpenAPI 3.1 document for the HTTP surface | | @ontrails/http/fetch | Shared Web Fetch createRouteHandler() and createFetchHandler() kernel | | @ontrails/http/bun | Bun-native createApp() and surface() binding | | @ontrails/http/testing | Owner-owned adapter conformance factory for HTTP adapter authors |

Adapter authoring

HTTP adapter authors should validate adapters through the owner-owned testing subpath instead of copying conformance behavior into each adapter:

import {
  createHttpAdapterConformanceCases,
  runConformance,
} from '@ontrails/http/testing';
import { myHttpAdapter } from './adapter.js';

runConformance(myHttpAdapter, createHttpAdapterConformanceCases());

The adapter under test provides a name and createApp(graph, options) method that returns an object with a Web Fetch-compatible fetch(request) handler. The conformance cases cover query and body input rendering, validation envelopes, public error redaction, request context, abort propagation, and webhook verification/parsing behavior.

Route derivation

Trail intent maps directly to HTTP method and input source:

| Trail field | HTTP method | Input source | | --- | --- | --- | | intent: 'read' | GET | Query string | | intent: 'write' | POST | JSON body | | intent: 'destroy' | DELETE | JSON body | | (none) | POST | JSON body |

Trail IDs map to paths: entity.show becomes /entity/show. Dots become slashes, everything lowercase.

Collision detection

deriveHttpRoutes detects when two trails would produce the same (method, path) pair and returns Result.err(ValidationError) describing both trail IDs. The surface() helper from @ontrails/hono throws on collision.

Resource resolution

Declared resources on each trail are resolved into the context before the implementation receives input.

Filtering

const result = deriveHttpRoutes(graph, {
  include: ['entity.**'],
  exclude: ['dev.**'],
});

* matches one dotted segment and ** matches any depth. Trails declared with visibility: 'internal' stay hidden unless you include their exact trail ID intentionally.

Request context and abort propagation

The execute function on each HttpRouteDefinition accepts optional requestId, abortSignal, and request context arguments. HTTP adapters should pass the request's AbortSignal so client disconnects propagate into trail execution, and pass headers in the request context when Bearer auth should resolve into ctx.permit.

HttpRouteDefinition

Each route definition produced by deriveHttpRoutes includes:

| Field | Type | What it is | | --- | --- | --- | | method | 'GET' \| 'POST' \| 'DELETE' | HTTP method | | path | string | Derived path (e.g. /entity/show) | | trailId | string | The trail ID this route was derived from | | inputSource | 'query' \| 'body' | Where to read input | | trail | Trail | The original trail definition | | execute | (input, requestId?, abortSignal?, context?) => Promise<Result> | Validates, layers, resolves request auth when configured, and runs the trail |

For GET routes on the Hono surface, repeated query keys are passed through as arrays (?tag=one&tag=two -> { tag: ['one', 'two'] }) while a single occurrence stays a scalar string. The adapter does not coerce singleton query values into arrays.

GET query values declared as numbers or booleans are converted at the HTTP boundary before schema validation. This includes primitive literals and union or nullable schemas whose non-null branches all resolve to the same primitive kind. Root object unions convert a field only when every branch that has its required fields present explicitly owns the field with the same primitive shape; otherwise the raw value is preserved. This keeps unknown and passthrough fields unchanged without choosing a union branch. Fields authored with Zod coercion receive the raw query value so their authored parser retains the same behavior as direct and library invocation; if any supported union branch for a field uses coercion, the boundary conservatively preserves that field. Numbers use JSON number syntax and must be finite; booleans accept the exact spellings true and false. Malformed values and the string null continue through normal validation and return a 400 response when the authored schema rejects them. Declared strings remain strings. Repeated keys for declared primitive arrays, including homogeneous union or nullable array schemas, apply the same conversion to each element, while a singleton remains a scalar and must satisfy the authored schema as-is.

For versioned trails, query conversion resolves the selected version's input schema. X-Trails-Version and X-Trail-Version headers take precedence over the trailVersion query field, matching execution. If a historical input field conflicts with a layer parameter name rendered for the current version, the boundary preserves raw query strings so it does not guess which schema owns the field.

Installation

These installation examples target Trails 0.2.1 on the normal npm release line.

bun add --exact @ontrails/[email protected] @ontrails/[email protected]
# or, for Bun-native serving:
bun add --exact @ontrails/[email protected]

Migration

Hono integration now lives in @ontrails/hono.

  • Replace import { trailhead } from '@ontrails/http/hono' with import { surface } from '@ontrails/hono'
  • Keep deriveHttpRoutes() and the route model imports on @ontrails/http