Migrate from v0.1.x Serve API
Move from the v0.1.x builder-first serve API to object-style query definitions with serve({ queries }).
Migrate from v0.1.x Serve API
If you are already using the v0.1.x builder-first serve API, the new path is:
- keep your existing
dbclient - convert each endpoint into
query({ ... }) - export
apifromserve({ queries })
The query builder itself does not change. Most of the migration is moving metadata out of the chained builder style and into object-style query definitions.
This guide is for migrating from the older serve API shown in v0.1.x Serve API to the current default path used in Quick Start.
What changes
Old style
const { define, queries, query } = initServe(...)export const api = define({ queries: queries({ ... }) })- query metadata lives on chained builders like
.describe(),.input(),.output()
New style
const { query, serve } = initServe(...)- define each query as
const myQuery = query({ ... }) - export
apifromserve({ queries: { myQuery } }) - keep route registration with
api.route(...)
What stays the same
- your typed
dbclient - the fluent ClickHouse query builder
ctx.db.execute()api.run(...)api.handlerapi.route(...)
Before and after
Legacy serve API
import { initServe } from '@hypequery/serve';
import { z } from 'zod';
import { db } from './client';
const { define, queries, query } = initServe({
context: () => ({ db }),
basePath: '/api/analytics',
});
export const api = define({
queries: queries({
activeUsers: query
.describe('Most recent active users')
.input(z.object({
limit: z.number().min(1).max(100).default(10),
}))
.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()
),
}),
});
api.route('/active-users', api.queries.activeUsers, { method: 'POST' });New serve API
import { initServe } from '@hypequery/serve';
import { z } from 'zod';
import { db } from './client';
const { query, serve } = initServe({
context: () => ({ db }),
basePath: '/api/analytics',
});
const activeUsers = query({
description: 'Most recent active users',
input: z.object({
limit: z.number().min(1).max(100).default(10),
}),
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(),
});
export const api = serve({
queries: { activeUsers },
});
api.route('/active-users', api.queries.activeUsers, { method: 'POST' });Migration steps
Keep your initServe() config
In most cases your context, basePath, auth setup, and other top-level runtime config can stay where it is. The main change is the return value you use:
// before
const { define, queries, query } = initServe({ ... });
// after
const { query, serve } = initServe({ ... });Convert each query from chained metadata to object-style config
Map the old chained methods to the new object keys:
| Legacy | New |
|---|---|
.describe('...') | description: '...' |
.input(schema) | input: schema |
.output(schema) | output: schema |
.query(fn) | query: fn |
Example:
// before
revenue: query
.describe('Weekly revenue totals')
.input(z.object({ startDate: z.string() }))
.output(z.object({ total: z.number() }))
.query(({ ctx, input }) => {
// ...
})
// after
const revenue = query({
description: 'Weekly revenue totals',
input: z.object({ startDate: z.string() }),
output: z.object({ total: z.number() }),
query: ({ ctx, input }) => {
// ...
},
});Replace define({ queries: queries(...) }) with serve({ queries })
Once your queries are constants, export the runtime layer like this:
export const api = serve({
queries: { revenue, activeUsers, topAccounts },
});This becomes the exported API object used by:
api.route(...)api.run(...)api.handlerapi.start(...)
Keep route registration and in-process execution
These usually do not change:
api.route('/revenue', api.queries.revenue, { method: 'POST' });
const result = await api.run('revenue', {
input: { startDate: '2026-01-01' },
});Common migration patterns
Multiple queries
const revenue = query({ ... });
const activeUsers = query({ ... });
const topAccounts = query({ ... });
export const api = serve({
queries: { revenue, activeUsers, topAccounts },
});Query-first local execution
The new API makes local execution more explicit:
const result = await activeUsers.execute({
input: { limit: 25 },
});You can still execute through the runtime:
const result = await api.run('activeUsers', {
input: { limit: 25 },
});When not to migrate yet
If you rely heavily on older builder-first per-query features that are still documented in the legacy section, keep using the legacy API for now.
Examples include:
- older per-query auth patterns
- older tenant override patterns
- builder-style middleware chains tied to legacy docs
If your current setup depends on those legacy patterns, migrate only after confirming the equivalent behavior in the current runtime path. The legacy docs remain available for that reason.
Recommended order
If your codebase is large, migrate in this order:
- convert one simple query
- export
apifromserve({ queries }) - verify
api.run(...)still works - verify HTTP routes still work
- migrate the remaining queries one by one
Next steps
- Start with the Quick Start if you want the new happy path
- Read Core Concepts for the new execution model
- Keep the v0.1.x Serve API open while migrating if you need to compare syntax