> hypequery

Core Concepts

Understand the three layers in hypequery: query builder, query definitions, and serve runtime.

hypequery has three layers:

  • db.table(...) is the typed ClickHouse query builder
  • query({ ... }) turns builder logic into a reusable contract
  • serve({ queries }) exposes those contracts through routes, docs, handlers, and runtime features

The mental model

Start with the query builder when the logic is local.

Move to query({ ... }) when that logic needs:

  • a stable name
  • typed input or output
  • validation
  • per-query auth or tenant rules
  • reuse across your codebase

Add serve({ queries }) when the same query needs:

  • HTTP routes
  • docs and OpenAPI
  • framework handlers
  • shared auth, tenant isolation, and runtime middleware

One compact example

import { initServe } from '@hypequery/serve';
import { z } from 'zod';
import { db } from './client';

const { query, serve } = initServe({
  context: () => ({ db }),
  basePath: '/api/analytics',
});

const latestUsers = query({
  description: 'Most recent active users',
  requiredRoles: ['admin'],
  input: z.object({
    limit: z.number().min(1).max(100).default(10),
  }),
  query: ({ ctx, input }) =>
    ctx.db
      .table('users')
      .select(['id', 'email', 'created_at'])
      .where('status', 'eq', 'active')
      .orderBy('created_at', 'DESC')
      .limit(input.limit)
      .execute(),
});

export const api = serve({
  queries: { latestUsers },
});

Inside query({ ... }), you still write normal builder code with ctx.db.table(...).

When to use each layer

  • Use the query builder when the query only lives in one place and you just want typed ClickHouse access.
  • Use query({ ... }) when the query should become a reusable, validated contract with its own input, metadata, and per-query rules.
  • Use serve({ queries }) when that contract should also become an API or shared runtime surface.

On this page