> hypequery

Serve API

API guide for defineServe, ServeConfig, and related utilities in @hypequery/serve

@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 runtime overview first.

defineServe

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

export const api = defineServe({
  queries: {
    activeUsers: {
      query: async ({ ctx }) => ctx.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', version: '1.2.0', info: { title: 'Analytics' } },
  context: async ({ request }) => ({ requestId: request.headers['x-request-id'] }),
  hooks: {
    onRequestStart: ({ queryKey, requestId }) => trace.begin(requestId, queryKey),
    onRequestEnd: ({ durationMs, queryKey }) => metrics.record(queryKey, durationMs),
    onError: ({ error, queryKey }) => logger.error(error, { queryKey }),
  },
});

ServeConfig options

Prop

Type

Query definitions (ServeQueryConfig)

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

Prop

Type

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:

Prop

Type

Utilities

normalizeHeaderMap

Normalize a plain header object to lower-case keys so you can do case-insensitive header lookups in auth strategies or middleware.

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

const headers = normalizeHeaderMap(request.headers);
const token = headers['authorization'];

getHeader

Read a header from a ServeRequest with case-insensitive and array-safe normalization.

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

const token = getHeader(request, 'x-tenant-key');

apiKeyAuth

Convenience adapter for header-based API key auth with clear missing/invalid errors.

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

const apiKeyStrategy = apiKeyAuth({
  header: 'x-tenant-key',
  validate: (key) => lookupTenant(key),
});

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.

Security hardening

CORS

Enable cross-origin resource sharing so browser clients (including @hypequery/react) can reach the server. Configure via the cors key in defineServe:

// Permissive — allow any origin (useful in development)
const api = defineServe({ queries, cors: true });

// Restrictive — fine-grained control
const api = defineServe({
  queries,
  cors: {
    origin: 'https://app.example.com',       // exact match; also accepts string[], or (origin) => boolean
    methods: ['GET', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true,                        // sets Access-Control-Allow-Credentials: true
    maxAge: 86400,                            // preflight cache duration in seconds
  },
});

OPTIONS preflight requests are handled automatically and return 204 No Content. CORS headers are injected on every response, including 404 and 429 errors.

Prop

Type

Rate limiting

rateLimit() is a middleware factory that protects endpoints from abuse. It works as a global middleware or can be scoped to individual queries.

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

// Global — applied to every endpoint
const api = defineServe({
  queries,
  middlewares: [
    rateLimit({ windowMs: 60_000, max: 100 }), // 100 req/min per IP
  ],
});

// Per-query — different limits per endpoint
const api = defineServe({
  queries: {
    expensiveReport: {
      query: async ({ ctx }) => { /* ... */ },
      middlewares: [
        rateLimit({
          windowMs: 60_000,
          max: 10,
          keyBy: (ctx) => ctx.auth?.tenantId ?? null, // per-tenant instead of per-IP
        }),
      ],
    },
  },
});

When the limit is exceeded the server responds with 429 Too Many Requests and includes Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.

Prop

Type

Custom store (Redis / distributed)

Implement RateLimitStore to share counters across processes:

import { RateLimitStore, rateLimit } from '@hypequery/serve';
import { Redis } from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

const redisStore: RateLimitStore = {
  async increment(key, windowMs) {
    const count = await redis.incr(key);
    if (count === 1) await redis.pexpire(key, windowMs);
    return count;
  },
  async getTtl(key) {
    const ttl = await redis.pttl(key);
    return ttl > 0 ? ttl : 0;
  },
  async reset(key) {
    await redis.del(key);
  },
};

const api = defineServe({
  queries,
  middlewares: [rateLimit({ store: redisStore, max: 500 })],
});

Body limits and request timeouts

These are configured on the Node server via api.start() or startNodeServer():

await api.start({
  port: 3000,
  bodyLimit: 2_097_152,          // 2 MB — returns 413 if exceeded (default: 1 MB)
  requestTimeout: 15_000,        // 15 s — returns 504 if handler stalls (default: 30 s)
  gracefulShutdownTimeout: 5_000, // 5 s drain window on SIGTERM (default: 10 s)
});

Prop

Type

Throwing structured errors from handlers

Use ServeHttpError to return a specific HTTP status and error type from any handler or middleware without leaking internal details:

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

const api = defineServe({
  queries: {
    sensitiveReport: {
      query: async ({ ctx, input }) => {
        if (!ctx.auth?.isPremium) {
          throw new ServeHttpError(403, 'UNAUTHORIZED', 'Upgrade required');
        }
        // ...
      },
    },
  },
});

The error reaches the client as a standard ErrorEnvelope. Unhandled Error instances (not ServeHttpError) are caught and returned as a generic 500 INTERNAL_SERVER_ERROR — "An unexpected error occurred" without leaking the original message, stack trace, or query details. The raw error is still delivered to the onError lifecycle hook for logging.

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

Prop

Type

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" | "PAYLOAD_TOO_LARGE" | "GATEWAY_TIMEOUT" | "SERVICE_UNAVAILABLE" | "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.

On this page