Query Definitions
Learn how to define type-safe queries using the hypequery serve framework
Basic Structure
A query definition wraps your metric logic with metadata, validation, and routing:
import { initServe } from '@hypequery/serve';
import { z } from 'zod';
import { db } from './client';
const { define, queries, query } = initServe({
context: () => ({ db }),
});
export const api = define({
queries: queries({
weeklyRevenue: query
.describe('Get weekly revenue totals')
.input(z.object({
startDate: z.string().datetime(),
endDate: z.string().datetime(),
}))
.output(z.object({
total: z.number(),
}))
.query(({ ctx, input }) =>
ctx.db
.table('transactions')
.where('date', 'gte', input.startDate)
.where('date', 'lte', input.endDate)
.sum('amount')
.execute()
),
}),
});
// Register HTTP route
api.route('/metrics/weekly-revenue', api.queries.weeklyRevenue);Query Configuration
Each query accepts the following options:
query (required)
The core function that executes your metric logic:
query: async ({ input, ctx }) => {
// input: validated request payload
// ctx: request context (auth, tenantId, locals, etc.)
return ctx.db.table('orders')
.where('created_at', 'gte', input.startDate)
.select('*')
.execute();
}inputSchema
Zod schema for request validation:
inputSchema: z.object({
startDate: z.string().datetime(),
endDate: z.string().datetime(),
status: z.enum(['pending', 'completed']).optional(),
})outputSchema
Zod schema for response typing and documentation:
outputSchema: z.object({
total: z.number(),
currency: z.string(),
breakdown: z.array(z.object({
date: z.string(),
amount: z.number(),
})),
})method
HTTP method for this query. Defaults to GET. You can set it on the definition or override it when registering the route:
// Inline on the query
method: 'POST'
// Or override later if exposing the route
api.route('/revenue', api.queries.revenue, { method: 'POST' });Pick one style per endpoint—if you specify both, the value passed to api.route takes priority.
name
Human-friendly display name for docs, OpenAPI, and api.describe() consumers. Defaults to the query key. Use this when the key is terse but you want richer presentation:
name: 'Weekly revenue (USD)'summary
Short description for OpenAPI documentation:
summary: 'Get weekly revenue totals'description
Detailed description for documentation:
description: 'Returns the sum of all transactions within a given date range, grouped by day'tags
Tags for grouping in OpenAPI/documentation:
tags: ['revenue', 'analytics', 'financial']middlewares
Endpoint-specific middleware:
middlewares: [
async (ctx, next) => {
console.log('Before query execution');
const result = await next();
console.log('After query execution');
return result;
},
]auth
Endpoint-specific authentication:
auth: async ({ request }) => {
const token = request.headers['x-api-key'];
if (token === 'secret') {
return { userId: '123', role: 'admin' };
}
return null;
}tenant
Tenant isolation configuration:
tenant: {
extract: (auth) => auth.organizationId,
required: true,
}cacheTtlMs
Sets the HTTP Cache-Control header that will be applied once you register this query as an HTTP route. This does not cache ClickHouse queries—it only tells downstream clients/CDNs how long they may reuse the HTTP response. Use the query builder's cache helpers if you want to memoize database results server-side.
cacheTtlMs: 60_000 // Cache for 1 minuteNeed to override it dynamically? Call ctx.setCacheTtl(ms) inside the handler to adjust the header per request (or pass null to force Cache-Control: no-store).
custom
Custom metadata:
custom: {
owner: 'data-team',
sla: '100ms',
criticality: 'high',
}Reusing Query Types
Need fully typed inputs/outputs elsewhere (React hooks, API routes, SDKs)? Use the helper types exported from @hypequery/serve:
import type { InferQueryInput, InferQueryOutput, InferQueryResult } from '@hypequery/serve';
import type { api } from './analytics/api';
type TripsInput = InferQueryInput<typeof api, 'tripsQuery'>; // input schema
type TripsResult = InferQueryResult<typeof api, 'tripsQuery'>; // builder return type
type TripsResponse = InferQueryOutput<typeof api, 'tripsQuery'>; // zod-derived type if providedUse InferQueryResult when you trust the builder's static typing (no schema required). InferQueryOutput reads the optional outputSchema, which is handy when runtime validation is the source of truth. Both helpers accept either the serve.define instance or a raw ServeQueriesMap, so you can infer types from any subset of queries.
Inline Execution Helpers
Calling await api.run('tripsQuery') (alias of api.execute) now returns the same type as InferQueryResult<typeof api, 'tripsQuery'>. For inline scripts or devtools, you can lean on the helper to annotate external call sites:
import { api } from './analytics/api';
import type { InferQueryResult } from '@hypequery/serve';
type Trips = InferQueryResult<typeof api, 'tripsQuery'>;
export async function listTrips(): Promise<Trips> {
return api.run('tripsQuery');
}If your query returns a builder directly, make sure to .execute() before returning so the type collapses to the parsed result instead of the intermediate QueryBuilder.
Organizing Queries
Single File Approach
For small projects, define all queries in one file:
// api/index.ts
import { initServe } from '@hypequery/serve';
import { db } from '../analytics/client';
const { define, queries } = initServe({
context: () => ({ db }),
});
export const api = define({
queries: queries({
revenue: { /* ... */ },
users: { /* ... */ },
orders: { /* ... */ },
}),
});
api.route('/metrics/revenue', api.queries.revenue);
api.route('/metrics/users', api.queries.users);
api.route('/metrics/orders', api.queries.orders);Module-based Approach
For larger projects, split by domain:
// metrics/revenue.ts
export const revenueQueries = {
weeklyRevenue: {
query: async ({ input }) => { /* ... */ },
inputSchema: z.object({ /* ... */ }),
},
monthlyRevenue: {
query: async ({ input }) => { /* ... */ },
},
};
// metrics/users.ts
export const userQueries = {
activeUsers: {
query: async ({ input }) => { /* ... */ },
},
};
// api/index.ts
import { initServe } from '@hypequery/serve';
import { db } from '../analytics/client';
import { revenueQueries } from './metrics/revenue';
import { userQueries } from './metrics/users';
const { define, queries } = initServe({
context: () => ({ db }),
});
export const api = define({
queries: queries({
...revenueQueries,
...userQueries,
}),
});
// Auto-register all routes
Object.entries(api.queries).forEach(([key, query]) => {
const path = `/metrics/${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
api.route(path, query);
});Factory Pattern
For queries with shared logic:
// lib/query-factory.ts
import { z } from 'zod';
export const createDateRangeQuery = (table: string, sumColumn: string) => ({
query: async ({ ctx, input }) => {
return ctx.db
.table(table)
.where('date', 'gte', input.startDate)
.where('date', 'lte', input.endDate)
.sum(sumColumn)
.execute();
},
inputSchema: z.object({
startDate: z.string(),
endDate: z.string(),
}),
outputSchema: z.object({
total: z.number(),
}),
});
// Use the factory
import { initServe } from '@hypequery/serve';
import { db } from '../analytics/client';
const { define, queries } = initServe({
context: () => ({ db }),
});
const api = define({
queries: queries({
revenue: createDateRangeQuery('transactions', 'amount'),
refunds: createDateRangeQuery('refunds', 'amount'),
}),
});Execution Modes
HTTP Execution
// Deploy as HTTP server
api.start({ port: 3000 });
// Or use as middleware in Next.js, Express, etc.
export default api.handler;Direct Execution
// Execute without HTTP layer
const result = await api.run('weeklyRevenue', {
input: {
startDate: '2025-01-01',
endDate: '2025-01-07',
},
});AI Agent Integration
// Expose to AI agents
const description = api.describe();
description.queries.forEach(query => {
console.log({
name: query.key,
description: query.summary,
parameters: query.inputSchema,
output: query.outputSchema,
});
});Global Configuration
Apply settings to all queries:
import { initServe } from '@hypequery/serve';
const { define, queries } = initServe({
basePath: '/api/v1',
// Global auth
auth: async ({ request }) => {
return verifyToken(request.headers['authorization']);
},
// Global tenant isolation
tenant: {
extract: (auth) => auth.tenantId,
required: true,
},
// Global middleware
middlewares: [
async (ctx, next) => {
const start = Date.now();
const result = await next();
console.log(`${ctx.metadata.path} took ${Date.now() - start}ms`);
return result;
},
],
// Global context factory
context: async ({ request, auth }) => ({
db: createDbConnection(),
logger: createLogger({ userId: auth?.userId }),
}),
// Lifecycle hooks
hooks: {
onRequestStart: async (event) => {
console.log(`Request started: ${event.queryKey}`);
},
onRequestEnd: async (event) => {
console.log(`Request completed in ${event.durationMs}ms`);
},
onError: async (event) => {
console.error(`Error in ${event.queryKey}:`, event.error);
},
},
});
const api = define({
queries: queries({
// Your queries inherit all global config
}),
});