Core Concepts
Understand the builder, reusable query definitions, and runtime layers in hypequery.
hypequery has three layers:
- a ClickHouse query builder
- reusable query definitions
- an optional runtime for routes, docs, auth, and framework integration
The three layers
1. Query builder
The builder is the lowest-level and most direct part of hypequery.
This is where you:
- connect to ClickHouse
- generate a typed schema
- build typed queries with
db.table(...) - execute them directly in your application
If your query only lives in one place, this may be all you need.
If you want to go deeper, see our Inside the Query Builder and ClickHouse Behavior pages in the reference section.
2. Reusable query definitions
The next layer turns local query logic into a reusable contract.
This is where you add things like:
- a stable name
- input and output schemas
- validation
- metadata
- per-query auth or tenant rules
The important idea is that the SQL-building logic does not move somewhere else. You still write normal builder code. This layer just gives that logic a durable interface.
3. Runtime
The runtime is optional.
It is the layer you add when reusable query definitions should also become part of a larger application surface, such as:
- HTTP routes
- generated docs
- OpenAPI
- middleware
- authentication
- framework handlers
This is not a separate query language. It is a delivery layer around the same query definitions.
The mental model
Start with a local typed query.
Move to a reusable query definition when the same query needs a stable interface.
Add the runtime when that interface should also be exposed through an application boundary such as HTTP or framework routing.
That progression looks like this:
| Layer | Use it when | Adds |
|---|---|---|
| Query builder | Query logic is local to one place | Typed ClickHouse query construction and execution |
| Reusable query definition | The query needs a stable contract | Validation, schemas, metadata, named execution, per-query rules |
| Runtime | The contract needs an application surface | Routes, docs, OpenAPI, auth, middleware, framework integration |
If you are unsure where to start, start with the builder.
One example, three stages
Stage 1: local builder query
const latestUsers = await db .table('users') .select(['id', 'email', 'created_at']) .where('status', 'eq', 'active') .orderBy('created_at', 'DESC') .limit(10) .execute();
This is the simplest useful hypequery program: a typed ClickHouse query executing directly in your code.
Stage 2: reusable query definition
const { query } = initServe({ context: () => ({ db }), }); export const latestUsers = query({ description: 'Most recent active users', 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(), });
The important change is not the SQL. The important change is that the query now has a defined interface with validation and metadata around the same builder logic.
Stage 3: runtime exposure
At this point the same reusable query can be mounted behind routes, docs, auth, middleware, or framework-specific handlers.
The runtime layer builds on the reusable query definition rather than replacing it.
const { query, serve } = initServe({ context: () => ({ db }), basePath: '/api/analytics', }); export const latestUsers = query({ description: 'Most recent active users', 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 }, });
Common transitions
Move from the builder to reusable query definitions when:
- the same query appears in more than one place
- the query needs validation
- the query needs metadata
- the query should have a stable name or contract
Move from reusable query definitions to the runtime when:
- the query should be callable over HTTP
- the query should appear in docs or OpenAPI
- the query should participate in shared auth or middleware
- the query should integrate with a framework surface
What this means in practice
hypequery is not one thing pretending to be many things.
It is one query model with three levels of use:
- local typed query building
- reusable query contracts
- optional delivery and runtime integration
That is why the same builder code remains central even as you move up the stack.