> hypequery

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 minute

Need 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 provided

Use 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
  }),
});

On this page