HTTP + OpenAPI
Expose hypequery metrics through HTTP handlers and autogenerated docs
HTTP + OpenAPI Delivery
hypequery can expose your queries as HTTP endpoints with automatically generated OpenAPI documentation.
Quick Example
Here's a complete example of exposing a query via HTTP:
1. Define your query:
// src/analytics/queries.ts import { initServe } from '@hypequery/serve'; import { z } from 'zod'; import { db } from './client'; const { query, serve } = initServe({ context: () => ({ db }), }); const revenue = query({ description: 'Get total revenue', output: z.object({ total: z.number(), count: z.number(), }), query: async ({ ctx }) => { const rows = await ctx.db .table('orders') .sum('amount', 'total') .count('order_id', 'count') .execute(); return rows[0]; }, }); export const api = serve({ queries: { revenue }, }); // Optional: expose the query at a custom path/method. // Without this, the query is already reachable at POST /queries/revenue. api.route('/revenue', api.queries.revenue, { method: 'POST' });
2. Start the server:
npx hypequery dev src/analytics/queries.ts # Server running at http://localhost:4000
hypequery devspins up the same HTTP server yourapi.handleruses. Every registered query is reachable atPOST /queries/<key>automatically;api.route(...)is only needed when you want a custom path or method (like the/revenueroute above). In production you can do the equivalent by callingawait api.start({ port }), or by embeddingapi.handlerinside your own framework/server if you don't want to rely on the CLI entry point.
3. Call your API:
curl -X POST http://localhost:4000/revenue # {"total": 125000, "count": 450}
That's it! Your query is now available as an HTTP endpoint with auto-generated OpenAPI docs at http://localhost:4000/docs.
Deployment Models
Embedded in Framework (Recommended for Web Apps)
Integrate hypequery directly into your web framework. Routes run on the same port as your application.
Supported frameworks:
- Next.js (Vercel adapter)
- Express
- Hono
- Any framework with standard Request/Response handlers
Example: Next.js
// app/api/hypequery/[...hq]/route.ts import { api } from '@/analytics/queries'; import { createFetchHandler } from '@hypequery/serve'; export const runtime = 'nodejs'; const handler = createFetchHandler(api.handler); export const GET = handler; export const POST = handler; export const OPTIONS = handler;
Example: Express
Use the Node adapter to mount hypequery alongside your existing routes:
// server.ts import express from 'express'; import { createNodeHandler } from '@hypequery/serve'; import { api } from './analytics/queries'; const app = express(); // Mount hypequery at /api/analytics app.use('/api/analytics', createNodeHandler(api.handler)); app.listen(3000);
Standalone Server
Run a dedicated hypequery server on its own port:
// src/analytics/server.ts import { api } from './queries'; await api.start({ port: 4000 });
api.start() starts the default Node server and returns a { stop() } handle for shutdown. If you need custom app routes, mount api.handler inside your own Express, Hono, or Next.js server instead of expecting an app object from start().
For local development, use the CLI:
npx hypequery dev src/analytics/queries.ts --port 4000
In production, start the server yourself with api.start({ port }) (above) or mount
api.handler inside your own framework — the CLI's dev command is for local
development, not production serving.
Edge/Fetch Runtimes
Deploy to edge platforms using the standard Fetch API:
// Cloudflare Worker import { api } from './analytics/queries'; import { createFetchHandler } from '@hypequery/serve'; const handler = createFetchHandler(api.handler); export default { fetch(request: Request) { return handler(request); }, }; // Vercel Edge Function import { api } from './analytics/queries'; import { createFetchHandler } from '@hypequery/serve'; const fetchHandler = createFetchHandler(api.handler); export default function handler(request: Request) { return fetchHandler(request); }
OpenAPI Documentation
Auto-generated OpenAPI specs are available at /openapi.json:
curl http://localhost:4000/openapi.json
Customize the generated OpenAPI document through serve({ openapi: ... }):
const api = serve({ queries: { revenue }, openapi: { title: 'My Analytics API', version: '2.0.0', servers: [ { url: 'https://api.example.com', description: 'Production' }, ], }, });
Semantic dataset and metric endpoints
Alongside hand-written queries, serve({ ... }) accepts metrics and datasets built with @hypequery/datasets. These generate governed POST endpoints automatically — you do not call api.route(...) for them, and they appear in /openapi.json and /docs like any other endpoint.
// src/analytics/queries.ts import { initServe } from '@hypequery/serve'; import { db } from './client'; import { Orders, revenue } from './datasets/orders'; const { serve } = initServe({ context: () => ({ db }), }); export const api = serve({ // Required whenever metrics or datasets are registered. queryBuilder: db, metrics: { revenue }, datasets: { orders: Orders }, });
This registers:
| Config | Generated endpoint |
|---|---|
metrics: { revenue } | POST /metrics/revenue |
datasets: { orders: Orders } | POST /datasets/orders/query |
Both validate the request body (dimensions, measures, filters, ordering, time grain) against the metric or dataset contract before executing. A requested limit above the endpoint's maxLimit is clamped to maxLimit rather than rejected. queryBuilder is required whenever metrics or datasets are present; it can also be supplied through context as context: () => ({ db }).
Customize the path prefixes with semanticPaths:
export const api = serve({ queryBuilder: db, metrics: { revenue }, datasets: { orders: Orders }, semanticPaths: { metrics: '/api/metrics', datasets: '/api/data', }, }); // POST /api/metrics/revenue // POST /api/data/orders/query
For per-entry options (auth, caching, limits) and request/response shapes, see Serve integration.
Pagination
Semantic endpoints support offset pagination. Send limit and offset in the request body, and the response reports the offset it served along with hasMore, so clients know whether to fetch the next page.
curl -X POST http://localhost:4000/datasets/orders/query \ -H 'content-type: application/json' \ -d '{ "dimensions": ["country"], "measures": ["revenue"], "limit": 50, "offset": 50 }'
In React, useInfiniteMetric and useInfiniteDataset consume hasMore/offset for you — see the React API reference.
Result metadata
Semantic endpoints return { data } by default. Opt into metadata (generated SQL, timing, row count, tenant, pagination) by sending includeMeta: true in the request body or the x-include-meta: true header, which switches the response to { data, meta }.
curl -X POST http://localhost:4000/datasets/orders/query \ -H 'content-type: application/json' \ -H 'x-include-meta: true' \ -d '{ "dimensions": ["country"], "measures": ["revenue"], "limit": 10 }'
Documentation UI
Serve interactive API documentation from the built-in /docs route, or customize the generated page with serve({ docs: ... }):
const api = serve({ queries: { revenue }, docs: { title: 'My API Docs', subtitle: 'Analytics runtime', darkMode: true, }, });
The runtime will expose that UI at /docs automatically once routes are registered.
If you want to host the docs HTML yourself, use the exported helper:
import { buildDocsHtml } from '@hypequery/serve'; app.get('/docs', (req, res) => { res.send(buildDocsHtml('/openapi.json', { title: 'My API Docs', subtitle: 'Analytics runtime', darkMode: true, })); });
Custom Routes
Register individual queries with custom paths and methods:
import { api } from './analytics/queries'; // Register with custom path api.route('/analytics/revenue', api.queries.revenue, { method: 'POST' }); // Register as GET api.route('/health', api.queries.healthCheck, { method: 'GET' }); // Use query metadata from definition api.route('/weekly', api.queries.weeklyRevenue);