Re-using Queries
Turn typed builder logic into reusable query definitions with the query function.
When to use
Use query({ ... }) when that builder logic needs to become a reusable application contract.
That usually means you want one or more of these:
- Typed input for callers
- Output validation
- Descriptions for docs and OpenAPI
- Tags and summaries for grouped docs
- Per-query auth and tenant rules
- A stable definition you can execute locally and later mount in
serve({ queries })
query({ ... }) does not replace the builder. It wraps builder logic so you can reuse it consistently.
The distinction
db.table(...)builds and runs a query directlyquery({ ... })turns that query into a reusable definitionserve({ queries })exposes those definitions through routes, docs, handlers, and runtime features
Example: wrap builder logic as a reusable query
Start by creating the shared runtime helpers:
import { initServe } from '@hypequery/serve'; import { z } from 'zod'; import { db } from './client'; const { query, serve } = initServe({ context: () => ({ db }), basePath: '/api/analytics', });
Then define a reusable query:
const activeUsers = query({ description: 'Most recent active users', summary: 'List active users', tags: ['users'], requiredRoles: ['admin', 'editor'], input: z.object({ limit: z.number().min(1).max(500).default(50), }), output: z.array(z.object({ id: z.string(), email: z.string(), created_at: z.string(), })), query: ({ ctx, input }) => ctx.db .table('users') .select(['id', 'email', 'created_at']) .where('status', 'eq', 'active') .orderBy('created_at', 'DESC') .limit(input.limit) .execute(), });
Inside query({ ... }), you still write normal builder code with ctx.db.table(...).
What query({ ... }) adds
Compared with a raw builder chain, query({ ... }) adds:
inputfor validated caller inputoutputfor validated response shapedescriptionandsummaryfor docstagsfor groupingrequiresAuth,requiredRoles, andrequiredScopesfor per-query access rulestenantfor per-query tenant overrides
Those fields do not change how you write the query itself. They add metadata and validation around it.
Reuse locally before HTTP
You can execute a query definition in-process before you ever add a route:
const rows = await activeUsers.execute({ input: { limit: 25 }, });
This is useful when multiple server-side parts of your app should share the same query contract without going over HTTP.
Add serve({ queries }) later
Once you have reusable definitions, you can expose them through the runtime:
export const api = serve({ queries: { activeUsers }, }); api.route('/active-users', api.queries.activeUsers, { method: 'POST' });
The key in queries: { activeUsers } becomes the stable query name inside the exported API.
If you need HTTP-specific behavior like method, add it when you route the query or in the runtime config.
Inserts
Insert rows into ClickHouse with a type-safe TypeScript API. Row shapes are derived from your schema, with nullable columns optional and value types checked at compile time.
Schemas
Validate ClickHouse query inputs and outputs with Zod. Use schemas for type-safe TypeScript APIs, runtime validation, and OpenAPI generation.