@hypequery/serve reference

The serve package turns your metrics into HTTP endpoints, docs, and OpenAPI artifacts. This page lists every exported primitive so you can wire the runtime intentionally. For a conceptual tour of delivery options, read the Serve overview first.

defineServe

import { defineServe } from '@hypequery/serve';

export const api = defineServe({
  queries: {
    activeUsers: {
      query: async ({ db }) => db.table('users').where('status', 'eq', 'active').count(),
      inputSchema: z.object({ region: z.string().optional() }),
      outputSchema: z.object({ total: z.number() }),
      method: 'POST',
      cacheTtlMs: 5_000,
      tags: ['users'],
      auth: async ({ request }) => verifySession(request),
    },
  },
  basePath: '/api/analytics',
  tenant: { extract: (auth) => auth?.accountId, column: 'account_id', mode: 'auto-inject' },
  auth: async ({ request }) => getAuthContext(request),
  middlewares: [logRequests],
  docs: { enabled: true, path: '/docs', title: 'Analytics API' },
  openapi: { enabled: true, path: '/openapi.json', info: { title: 'Analytics', version: '1.2.0' } },
  context: async ({ request, auth }) => ({ requestId: request.headers['x-request-id'], auth }),
  hooks: {
    onRequestStart: ({ queryKey, requestId }) => trace.begin(requestId, queryKey),
    onRequestEnd: ({ durationMs, queryKey }) => metrics.record(queryKey, durationMs),
    onError: ({ error, queryKey }) => logger.error(error, { queryKey }),
  },
});

ServeConfig options

PropertyTypeDefaultDescription
queriesServeQueriesMaprequiredMap of query definitions (either ServeQueryConfig objects or executable functions).
basePathstring"/api/analytics"Prefix applied to every generated route; docs/OpenAPI inherit this prefix.
middlewaresServeMiddleware[][]Global middleware stack executed before every endpoint.
auth`AuthStrategyAuthStrategy[]`[]
tenantTenantConfigundefinedGlobal tenant isolation config; individual queries can override or opt out.
docsDocsOptions{ enabled: true, path: "/docs" }Built-in Redoc UI served at ${basePath}${path}. Disable if you host your own docs.
openapiOpenApiOptions{ enabled: true, path: "/openapi.json" }OpenAPI JSON served at ${basePath}${path}; point custom UIs/clients here.
contextServeContextFactoryundefinedObject or factory merged into ctx for handlers/middlewares.
hooksServeLifecycleHooks{}Lifecycle callbacks (onRequestStart, onRequestEnd, onError, onAuthFailure).
queryLoggingboolean | 'json' | ServeQueryEventCallbackundefinedOpt-in query logging. true logs human-readable text, 'json' logs structured JSON for aggregators (Datadog, CloudWatch), or pass a custom (event) => void. Disabled (zero overhead) when omitted.
slowQueryThresholdnumberundefinedWarn via console.warn when a completed query exceeds this many milliseconds. Works independently of queryLogging.

Query definitions (ServeQueryConfig)

You can describe endpoints declaratively or pass executable functions directly. When using the config form the following fields are available:

FieldTypeDefaultPurpose
queryExecutableQueryrequiredResolver (({ input, ctx }) => result) or legacy (input, ctx) function/object with run.
methodHttpMethod"GET"HTTP verb for the endpoint.
namestringquery keyHuman-readable label shown in docs, OpenAPI, and api.describe().
inputSchema / outputSchemaZod schemasz.any()Validation + OpenAPI shape for request/response payloads.
middlewaresServeMiddleware[][]Endpoint-specific middleware stack (runs after global middlewares).
authAuthStrategy | nullinherits globalPer-endpoint auth override; returning null rejects the request.
requiresAuthbooleanundefinedExplicitly require or skip authentication. Set by .requireAuth() or .public().
requiredRolesstring[]undefinedRoles the user must have (OR semantics). Checked after auth. Returns 403.
requiredScopesstring[]undefinedScopes the user must have (AND semantics). Checked after auth. Returns 403.
tenantTenantConfiginherits globalPer-endpoint tenant config to opt out or override the global config.
cacheTtlMsnumber | nullnullSets cache-control headers / exposes cache helpers.
summary / description / tagsstring, string[]undefinedMetadata surfaced in OpenAPI and docs.
customRecord<string, unknown>undefinedArbitrary metadata also emitted via api.describe().

Executable functions can be passed directly in queries when you just need a handler—defineServe wraps them in a default configuration.

Builder API

defineServe returns a ServeBuilder with composable runtime helpers:

MemberDescription
builder.route(path, endpoint, options?)Register additional paths/methods for an existing endpoint. Lets you reuse the same query behind multiple URLs.
builder.use(middleware)Push a global middleware executed before every handler. Useful for logging, tracing, or mutating context.
builder.useAuth(strategy)Append an AuthStrategy. If at least one strategy exists the docs/OpenAPI metadata reflect requiresAuth.
builder.execute(key, { input, context, request })Run a query in-process without HTTP. Perfect for SSR, cron jobs, or tests. Throws when the endpoint returns an error response.
builder.run(key, options?)Alias of builder.execute (same signature) for in-process execution.
builder.describe()Returns a structured description (key, path, method, metadata) for downstream tooling.
builder.queryLoggerServeQueryLogger instance for subscribing to endpoint execution events. See Observability below.
builder.handlerLow-level ServeHandler function you can pass into any adapter (Node, Fetch, Edge).
builder.start(options?)Convenience helper that starts the default Node server (internally uses startNodeServer). Resolves to { stop() }.

Procedure builder (query.*)

When using initServe, the returned query object is a fluent builder for defining endpoints with chained configuration. All methods return a new builder instance (immutable chaining):

MethodDescription
.input(schema)Set the Zod input schema.
.output(schema)Set the Zod output schema.
.describe(text)Set the description.
.name(text)Set the human-readable name.
.summary(text)Set the OpenAPI summary.
.tag(tag) / .tags(tags)Append tag(s) for OpenAPI grouping.
.method(verb)Set the HTTP method (GET, POST, etc.).
.cache(ttlMs)Set cache TTL for Cache-Control headers.
.auth(strategy)Set a per-endpoint auth strategy (or null to disable).
.requireAuth()Require authentication. Rejects with 401 when no credentials.
.requireRole(...roles)Require at least one role (OR). Rejects with 403. Implies .requireAuth().
.requireScope(...scopes)Require all scopes (AND). Rejects with 403. Implies .requireAuth().
.public()Explicitly skip auth, even when global strategies are configured.
.tenant(config)Set per-endpoint tenant isolation config.
.custom(metadata)Merge custom metadata for api.describe() consumers.
.use(...middlewares)Append endpoint-specific middleware.
.query(executable)Terminal method—returns the finalized ServeQueryConfig.

Auth guard semantics

GuardCheckHTTP statusError type
.requireAuth()ctx.auth must be non-null401UNAUTHORIZED
.requireRole('admin', 'editor')ctx.auth.roles must contain at least one403FORBIDDEN
.requireScope('read:x', 'write:x')ctx.auth.scopes must contain all403FORBIDDEN
.public()Skip auth enforcement

Guards are enforced in the pipeline after authentication and before tenant isolation, input validation, and middleware execution.

Auth guard middleware exports

For use with .use() or api.use() when you prefer middleware composition over the builder chain:

import {
  requireAuthMiddleware,
  requireRoleMiddleware,
  requireScopeMiddleware,
} from '@hypequery/serve';
HelperSignatureBehavior
requireAuthMiddleware()() => ServeMiddlewareThrows 401 if ctx.auth is null.
requireRoleMiddleware(...roles)(...string[]) => ServeMiddlewareThrows 403 if no role matches (OR).
requireScopeMiddleware(...scopes)(...string[]) => ServeMiddlewareThrows 403 if any scope is missing (AND).

Runtime helpers

  • serveDev(api, options?) – Launches a Node server (defaults to localhost:4000) with query logging always enabled (logs completed/errored requests to the terminal). Accepts port, hostname, signal, logger, and quiet.
  • Node adaptercreateNodeHandler converts a ServeHandler into a Node (req, res) listener, and startNodeServer boots an HTTP server with graceful shutdown helpers.
  • Fetch/edge adaptercreateFetchHandler produces a (request: Request) => Response for edge runtimes, Cloudflare Workers, Remix loaders, etc.
  • Vercel adapterscreateVercelEdgeHandler wraps the fetch adapter for Edge Functions, while createVercelNodeHandler reuses the Node adapter for the Node runtime.

Documentation + OpenAPI utilities

  • buildOpenApiDocument(endpoints, options?) – Generates an OpenAPI 3.1 document from any list of ServeEndpoints. Use it to persist specs or feed schema registries.
  • buildDocsHtml(openapiUrl, docsOptions?) – Produces the Redoc-powered HTML served at /docs. You can host the markup yourself by calling this helper directly.

Observability

Every response includes an X-Request-Id header (generated or echoed from the incoming x-request-id / x-trace-id). Use it to correlate HTTP responses with logs.

Query logging

Enable query logging to observe endpoint execution in production or during development.

// Human-readable text to stdout
const api = defineServe({ queries, queryLogging: true });
//   ✓ GET /api/analytics/revenue → 200 (12ms)
//   ✗ POST /api/analytics/report → 500 (3ms) — Connection refused

// Structured JSON for log aggregators
const api = defineServe({ queries, queryLogging: 'json' });
// {"level":"info","msg":"GET /api/analytics/revenue","requestId":"...","endpoint":"revenue","status":200,"durationMs":12,"timestamp":"..."}

// Custom callback (ship to Datadog, Sentry, etc.)
const api = defineServe({
  queries,
  queryLogging: (event) => {
    datadogLogs.logger.info(event.endpointKey, {
      duration: event.durationMs,
      status: event.responseStatus,
    });
  },
});

When queryLogging is omitted (the default), no listeners are registered and the emit path is skipped entirely — zero runtime overhead.

In development, serveDev() always subscribes its own terminal logger regardless of this setting.

Slow query warnings

Flag queries that exceed a duration threshold:

const api = defineServe({
  queries,
  queryLogging: 'json',
  slowQueryThreshold: 2000, // ms
});
// console.warn: [hypequery/slow-query] GET /api/analytics/report (report) took 3400ms (threshold: 2000ms)

slowQueryThreshold registers an independent listener that calls console.warn, so it works alongside any queryLogging mode (or even without it — set slowQueryThreshold alone if you only want warnings).

Programmatic access

Use api.queryLogger to subscribe manually:

const api = defineServe({ queries });

// Subscribe to all events
const unsubscribe = api.queryLogger.on((event) => {
  if (event.status === 'completed') {
    histogram.record(event.durationMs);
  }
});

// Check listener count (for diagnostics)
api.queryLogger.listenerCount; // 1

// Clean up
unsubscribe();

ServeQueryEvent

FieldTypeDescription
requestIdstringUnique request identifier (matches X-Request-Id header).
endpointKeystringThe query key from defineServe({ queries: { ... } }).
pathstringHTTP path of the endpoint.
methodstringHTTP method (GET, POST, etc.).
status'started' | 'completed' | 'error'Lifecycle phase.
startTimenumberDate.now() when the request began.
endTimenumber?Date.now() when the request finished (completed/error only).
durationMsnumber?Wall-clock duration in milliseconds (completed/error only).
inputunknown?Parsed input payload.
responseStatusnumber?HTTP status code of the response.
errorError?Error instance (error events only).
resultunknown?Query result (completed events only).

Formatting utilities

Two built-in formatters are exported for use in custom logging setups:

import { formatQueryEvent, formatQueryEventJSON } from '@hypequery/serve';

// Human-readable:  "  ✓ GET /api/analytics/revenue → 200 (12ms)"
formatQueryEvent(event);

// Structured JSON:  '{"level":"info","msg":"GET /api/analytics/revenue",...}'
formatQueryEventJSON(event);

Both return null for started events (only format completions and errors).

Types worth knowing

  • ServeMiddleware(ctx, next) => result. Mutate context, emit logs, wrap cache, etc.

  • AuthStrategy({ request, endpoint }) => auth | null. Compose multiple strategies to support API keys, JWTs, and tenant lookups.

  • TenantConfig – Enforce tenant isolation by extracting IDs, requiring presence, and optionally auto-injecting filters.

  • ServeLifecycleHooks – Observe every request for logging/metrics/tracing.

  • ServeQueryLogger – Event emitter for endpoint executions. Exposes .on(callback), .listenerCount, and .removeAll().

  • ServeQueryEvent – Event payload emitted during each endpoint lifecycle (started, completed, error). See Observability for the full field list.

  • ErrorEnvelope – Shape of errors returned from hypequery endpoints:

    {
      "error": {
        "type": "VALIDATION_ERROR" | "UNAUTHORIZED" | "FORBIDDEN" | "QUERY_FAILURE" | "CLICKHOUSE_UNREACHABLE" | "RATE_LIMITED" | "NOT_FOUND" | "INTERNAL_SERVER_ERROR",
        "message": "Human-friendly summary",
        "details": {
          "issues": [ /* zod validation errors */ ],
          "reason": "missing_credentials",
          "queryId": "...",
          // ... provider-specific metadata
        }
      }
    }

    Use this structure when surfacing errors to clients or agents so they can branch on the type field.

With these pieces you can embed analytics directly in your app, expose an HTTP API, or plug the same definitions into edge runtimes without rewriting handlers.