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.