@samveys/http

Version
1.0.3Nov 5, 2026
Install
pnpm add @samveys/http

From packages/http/README.md in the framework repository.

@samveys/http provides a typed router, HTTP kernel, Node server driver, middleware pipeline, schema validation, response conversion, and HTTP plugin. It uses the Fetch API Request and Response types internally and requires Node.js ≥ 26.13.0. The examples below are checked against the public exports and types (tested).

1. Install

Samveys packages are internal-only. Use the organization’s restricted registry and workspace tooling; never publish or install these packages through the public npm registry. In this monorepo, @samveys/http is a workspace package.

2. Quick start

Register the http() plugin with the app, add routes to its router, then construct and listen on the server. createHttpServer() defaults to the Node driver, host 0.0.0.0, and port 3333.

ts
import { createApp } from '@samveys/core'
import { createHttpServer, get, http } from '@samveys/http'

const app = createApp({
  plugins: [http({ host: '127.0.0.1', port: 3333 })],
  processEnv: {},
})
await app.run()

const router = app.container.make('http.router')
router.controller({
  kind: 'controller',
  name: 'health',
  prefix: '/health',
  use: [],
  routes: { show: get('/', () => ({ status: 'ok' })) },
})

const server = await createHttpServer(app)
await server.listen()

A full application may instead use defineController() and createRouter() to define and mount routes before creating the HTTP kernel or starting the server.

3. Routing

Routes support literal segments and named parameters; route patterns do not accept regular expressions. A wildcard must be the final segment.

Pattern syntaxMeaningExample matched pathHandler parameter
/usersLiteral path/usersNone
/:idRequired segment/users/42 with prefix /usersparams.id: string
/:id?Optional segment/users or /users/42params.id?: string
/*Trailing wildcard/files/a/bparams.wildcard: string
/*nameNamed trailing wildcard/files/a/bparams.name: string

Invalid regex patterns raise SVS_E045. get, post, put, patch, and del create route definitions; routes provides the same builders as an object, with routes.delete for DELETE.

ts
import { createRouter, defineController, get, post } from '@samveys/http'

const users = defineController('users', {
  prefix: '/users',
  routes: {
    show: get('/:id', ({ params }) => ({ id: params.id })),
    create: post('/', ({ response }) => response.created({ created: true })),
  },
})
const router = createRouter()
router.controller(users)

4. Controllers and route files

defineController(name, { prefix, use, routes }) groups named route definitions. By default, route names are <controller>.<key>; a route name option overrides that. defineRoutes(register) creates a route file, commonly exported as a default value and loaded through ServerConfig.routes.

ts
import { defineRoutes } from '@samveys/http'

export default defineRoutes((router) => {
  router.get('/health', () => ({ status: 'ok' }))
})

A router can also register inline routes or nest group({ prefix, use, plugins, as }, callback) scopes.

5. Middleware

Middleware has the shape (ctx, next) => Response | Promise<Response>. It may return early or call next() and modify the response. Execution order is global, group, controller, then route; after next() resolves, unwinding occurs in reverse order. A middleware can store request-local scratch data on ctx.state.

ts
import type { Middleware } from '@samveys/http'

const requireAuth: Middleware = (ctx, next) => {
  if (!ctx.headers.has('authorization')) {
    return Promise.resolve(Response.json({ error: 'Unauthorized' }, { status: 401 }))
  }
  return next()
}

6. Validation

Set params, query, or body schemas in route options. Samveys validator schemas implement Standard Schema v1; other Standard Schema implementations work directly, including asynchronous validators. An adapter registered with extend.validator() can integrate a schema library that does not implement Standard Schema.

Validation failure returns 422 with application/problem+json, code SVS_E042, and pointers such as #/body/email. Param, query, and body issues are collected together. The parsed schema output is the type received by the handler.

ts
import { schema } from '@samveys/validator'
import { createRouter, post } from '@samveys/http'

const createUser = schema.object({
  email: schema.string().email(),
  age: schema.coerceNumber().int().optional(),
})
const router = createRouter()
router.post('/users', { body: createUser }, ({ body }) => ({ email: body.email }))

7. Response schemas

Set response in route options to validate handler data before sending. By default the validator’s object schema strips keys not present in the schema. Invalid output returns 500 (SVS_E050) and does not expose the invalid data. An explicit Response returned by a handler is sent as-is.

ts
import { schema } from '@samveys/validator'
import { createRouter, get } from '@samveys/http'

const publicUser = schema.object({ id: schema.number(), name: schema.string() })
const router = createRouter()
router.get('/me', { response: publicUser }, () => ({ id: 1, name: 'Ada', passwordHash: 'not sent' }))

8. Errors

Framework errors are returned as RFC 9457 Problem Details, with content type application/problem+json. Responses include type, title, status, stable code, and requestId; 422 adds an errors array. Unhandled 500 details are hidden unless debug mode permits them and the error is logged with its request ID.

CaseStatus and codeExample response fields
No matching route404, SVS_E040{"type":"urn:samveys:error:SVS_E040","title":"Not Found","status":404,"code":"SVS_E040","requestId":"…"}
Schema validation failed422, SVS_E042{"type":"urn:samveys:error:SVS_E042","title":"Validation failed","status":422,"code":"SVS_E042","errors":[{"pointer":"#/body/email","detail":"Invalid email address"}]}
Unexpected handler error500, SVS_E049{"type":"urn:samveys:error:SVS_E049","title":"Internal Server Error","status":500,"code":"SVS_E049","requestId":"…"}

Development error page

When app.debug is on (by default only in the local environment, which svs serve sets), a 5xx response to a client whose Accept header starts with text/html is an HTML page instead of Problem Details. It shows the error and its cause chain, the SVS_E* explanation and fix, the stack, and the source lines around the first application frame. Every value is HTML-escaped, and the page is sent with cache-control: no-store and a CSP that allows only its inline styles. API clients, 4xx responses, and apps with debug off always get Problem Details.

9. Security defaults

ControlDefault behavior
Security headersCSP, X-Frame-Options: DENY, Referrer-Policy: no-referrer, and HSTS over HTTPS or trusted-proxy HTTPS.
Internal request headersStrip x-samveys-* prefixes and always strip x-middleware-subrequest; configurable through internalHeaderPrefixes.
Handler timeout30 seconds; aborts ctx.signal and responds with 503 (SVS_E044).
Request body limit1 MiB for JSON, form and text bodies (bodyLimit can override).
Request dataParsed params, query and body structures are prototype-less; unsafe keys are filtered.

The timeout and body limit can also be configured per route with timeout and bodyLimit.

Compression

Responses are compressed by default according to Accept-Encoding, preferring zstd, then brotli, then gzip; q-values and * are honoured. The body is streamed through the encoder, never buffered. Content-Length is removed, Vary: Accept-Encoding is added, and a strong ETag becomes weak.

A response is sent uncompressed when it is HEAD, 1xx, 204, or 304; already has Content-Encoding; has Cache-Control: no-transform; is text/event-stream (SSE); is already-compressed media (images other than SVG, video, audio, WOFF fonts, ZIP, gzip, zstd, 7z, RAR, PDF, or application/octet-stream); or has a known Content-Length under the threshold.

ts
import type { ServerConfig } from '@samveys/http'

export const server: ServerConfig = {
  compression: { encodings: ['br', 'gzip'], threshold: 2048 }, // or `false` to turn it off
}

Tracing

Each request runs inside the samveys:http.request TracingChannel. The context object carries span, an OpenTelemetry-style description named {method} {http.route} (GET /users/:id, never the raw path), with http.request.method, http.route, url.path, url.scheme and http.response.status_code attributes. 5xx responses are marked status: 'error' with error.type; 4xx responses and client cancellations are not. An APM can subscribe without importing Samveys:

ts
import { subscribe } from 'node:diagnostics_channel'

subscribe('tracing:samveys:http.request:asyncEnd', (message) => {
  const { span } = message as { span: { name: string; status: string; attributes: Record<string, unknown> } }
  console.log(span.name, span.attributes['http.response.status_code'], span.status)
})

10. Cancellation

ctx.signal is aborted when the client disconnects or when the request timeout expires. Pass it to downstream work that accepts an AbortSignal so that work can stop with the request.

ts
import { createRouter, get } from '@samveys/http'

const router = createRouter()
router.get('/slow', async ({ signal }) => {
  const response = await fetch('https://service.example/data', { signal })
  return response.json()
})

11. Hooks

HTTP hooks are added to the core hook registry by this package. Hook listeners run sequentially. intercept can return a Response to skip routing or the handler; filter can replace the final response.

HookKind / purpose
http:requestIntercept before routing; return a Response to short-circuit.
http:routeMatchedNotification after a route is selected.
http:beforeHandlerIntercept before invoking the route handler.
http:responseFilter and replace the final Response.
http:errorNotification for request errors.
http:upgradeNotification for WebSocket upgrade attempts.

12. Route-scoped plugins

Register a plugin with { plugin, scope: 'routes' }, then enable it on a router group with plugins. Hooks owned by the plugin run only for routes inside enabled groups. Plugins are global by default.

ts
import { definePlugin } from '@samveys/core'
import { createRouter } from '@samveys/http'

const audit = definePlugin({ name: '@acme/audit' })
const router = createRouter()
router.group({ prefix: '/admin', plugins: [audit] }, (admin) => {
  admin.get('/overview', () => ({ ok: true }))
})

13. route() and route type generation

route(name, params?) builds a path from the router bound as http.router in the active container/request scope. Use routeFrom(router, name, params?) when holding a router outside that scope. generateRouteTypes(router.list()) returns declarations for .samveys/types/routes.d.ts; the RouteRegistry interface can then provide compile-time route names and parameter types. svs route:types runs this for the routes mounted by the http() plugin and writes the file; include .samveys/types/**/*.d.ts in your tsconfig.json.

14. Driver conformance suite

@samveys/http/conformance is a node:test suite that every HttpDriver must pass. It starts a real server with the driver and sends requests over real sockets. The suite covers 29 cases in 10 areas: routing, middleware order, validation, Problem Details errors, security headers, streaming without buffering, WebSocket upgrade, timeouts, cancellation, and compression. The built-in node driver, @samveys/fastify and @samveys/hono pass every case (tested).

ts
import { node } from '@samveys/http'
import { runHttpConformance } from '@samveys/http/conformance'

runHttpConformance({
  driver: () => node(),
  // A driver without WebSocket support records the area as skipped, with the reason:
  // skip: { upgrade: 'driver has no upgrade support' },
})

Run the file with node --test. conformanceCases() returns the case list without running it. The suite needs @samveys/testing installed (an optional peer); the main @samveys/http entry never loads it.

15. SSE, WebSocket, deprecation and idempotency

sse(ctx, source) streams text/event-stream: the source gets the client's Last-Event-ID to resume, and a keep-alive : ping line goes out every 15 s. websockets({ routes: [ws(path, options, handlers)] }) serves RFC 6455 WebSockets on the same port with no dependencies. An Origin allow-list (403) and auth (401) run before the handshake. Messages can be schema-validated, size- and rate-limited. Every socket gets close code 1001 on shutdown. The server passes the Autobahn TestSuite: 301 cases, 0 failed, with permessage-deflate (12., 13.) excluded because it is not implemented (tested).

ts
import { createApp } from '@samveys/core'
import { http, sse, websockets, ws } from '@samveys/http'

export const app = createApp({
  name: 'live',
  plugins: [
    http(),
    websockets({
      routes: [ws('/chat', { origins: ['https://app.example.com'] }, { message: (socket, data) => socket.send(data as string) })],
    }),
  ],
})

export const events = (ctx: Parameters<typeof sse>[0]) =>
  sse(ctx, async function* ({ lastEventId }) {
    yield { id: Number(lastEventId ?? 0) + 1, event: 'tick', data: { at: Date.now() } }
  })

Two route options cover API lifecycle:

  • deprecated: { at, sunset?, link? } adds Deprecation (RFC 9745), Sunset (RFC 8594) and Link: rel="deprecation" to every response of the route.
  • idempotent: true (experimental) replays the stored response for a repeated Idempotency-Key. It returns 409 while the first request still runs, and 422 when the same key arrives with another body. A 5xx is not stored. It needs a store, or the cache binding from @samveys/cache.

describeRoutes(router) returns each route with its schemas, for tools such as @samveys/openapi.

16. Reference

Value exports from @samveys/http:

ExportPurpose
createHttpKernelCreate a fetch-capable HTTP kernel for an app.
resolveServerConfigApply and validate server configuration defaults.
createRouterCreate a router for routes and controllers.
defineControllerDefine a named route controller.
defineRoutesDefine a lazy-loadable route file.
delBuild a DELETE route.
getBuild a GET route.
patchBuild a PATCH route.
postBuild a POST route.
putBuild a PUT route.
routesCollect route builders, with delete for DELETE.
nodeCreate the built-in Node HTTP driver.
createHttpServerCreate a server handle with listen() and graceful close().
httpCreate the core HTTP plugin.
generateRouteTypesGenerate RouteRegistry declaration text.
routeBuild a path using the current router binding.
routeFromBuild a path from an explicit router.
problemResponseConvert a problem object to a Response.
toResponseConvert handler output to a Response.
compilePatternCompile and validate a supported path pattern.
negotiateEncodingPick the response encoding from an Accept-Encoding header.
compressResponseCompress a Response with one encoding, applying the skip rules.