> hypequery

Builder-to-Serve Migration

Migrate direct query-builder code into the v0.1.x builder-first serve API.

Migrate from Builder to Serve

This page documents migration into the v0.1.x builder-first Serve API. New projects should usually start in Quick Start. For the current execution model, read Core Concepts.

Already using the query builder? Adding the serve framework takes 10 minutes and requires zero query code changes.

The best part: Your existing query builder code works exactly the same in serve. Same syntax, same types, same results.


Why Upgrade?

You Need Serve If...

  • Your team is growing and you need access controls
  • You want to expose analytics as HTTP APIs
  • You're building multi-tenant applications
  • You need API documentation and governance
  • You want automatic React hooks and SDKs

Stay With Builder If...

  • You're building internal tools (no external API consumers)
  • You're a solo developer or small team
  • You don't need HTTP endpoints (just queries)
  • You prefer full control over execution

Install Serve

npm install @hypequery/serve zod

Optional: Install React integration if you want auto-generated hooks:

npm install @hypequery/react

Wrap Your Queries

Before (Builder only):

// lib/db.ts
import { createQueryBuilder } from '@hypequery/clickhouse';
import { createClient } from '@clickhouse/client';

const client = createClient({
  host: 'http://localhost:8123',
  username: 'default',
  password: '',
  database: 'analytics'
});

export const db = createQueryBuilder({ client });

// lib/queries.ts
export async function getActiveUsers() {
  return await db
    .table('users')
    .where('active', true)
    .execute();
}

After (Serve + Builder):

// lib/db.ts - Keep as is
import { createQueryBuilder } from '@hypequery/clickhouse';
import { createClient } from '@clickhouse/client';

const client = createClient({
  host: 'http://localhost:8123',
  username: 'default',
  password: '',
  database: 'analytics'
});

export const db = createQueryBuilder({ client });

// api/analytics.ts - NEW: Wrap with serve
import { initServe } from '@hypequery/serve';
import { db } from '../lib/db';

const { define, queries, query } = initServe({
  context: () => ({ db }),
});

export const api = define({
  queries: queries({
    activeUsers: query
      .describe('Get all active users')
      .query(async ({ ctx }) => {
        // Same builder syntax!
        return ctx.db
          .table('users')
          .where('active', true)
          .execute();
      }),
  }),
});

What Changed:

  • ✅ Kept your existing db instance
  • ✅ Wrapped queries with initServe and query()
  • No changes to query logic—same .table(), .where(), .execute()

Add HTTP Handler

Choose your platform:

// app/api/analytics/[...path]/route.ts (Next.js App Router)
import { createFetchHandler } from '@hypequery/serve';
import { api } from '@/api/analytics';

const handler = createFetchHandler(api.handler);

export const runtime = 'nodejs';
export const GET = handler;
export const POST = handler;
export const OPTIONS = handler;

// Or with Node.js/Express:
import { createNodeHandler } from '@hypequery/serve';
import { api } from '@/api/analytics';

const handler = createNodeHandler(api.handler);

app.use('/api/analytics', handler);

Test Your API

curl http://localhost:3000/api/analytics/activeUsers

You now have:

  • ✅ Type-safe HTTP endpoint
  • ✅ Auto-generated OpenAPI documentation
  • ✅ Request validation
  • ✅ Same query logic as before

Advanced Features

Add Authentication

import { initServe } from '@hypequery/serve';

const { define, queries, query } = initServe({
  context: () => ({ db }),
  auth: async ({ request }) => {
    const token = request.headers.authorization;
    if (token === `Bearer ${process.env.ADMIN_TOKEN}`) {
      return { userId: 'admin-1', roles: ['admin'] };
    }
    return null;
  },
});

export const api = define({
  queries: queries({
    adminOnly: query
      .requireRole('admin')
      .query(async ({ ctx }) => {
        return ctx.db
          .table('sensitive_data')
          .execute();
      }),
  }),
});

Add Multi-Tenancy

import { initServe } from '@hypequery/serve';

const { define, queries, query } = initServe({
  context: () => ({ db }),
  tenant: {
    extract: (auth) => auth?.tenantId,
    column: 'tenant_id',
    mode: 'auto-inject',
  },
  auth: async ({ request }) => {
    const tenantId = request.headers['x-tenant-id'];
    if (!tenantId || Array.isArray(tenantId)) return null;
    return { userId: 'user-1', tenantId };
  },
});

export const api = define({
  queries: queries({
    customerData: query
      .query(async ({ ctx }) => {
        return ctx.db
          .table('orders')
          .execute();
      }),
  }),
});

Generate React Hooks

// ui/components/Dashboard.tsx
import { createHooks } from '@hypequery/react';
import type { ApiDefinition } from '@/api/analytics';

const { useQuery } = createHooks<ApiDefinition>({
  baseUrl: '/api/analytics',
});

export function Dashboard() {
  const { data, isLoading } = useQuery('activeUsers');

  if (isLoading) return <div>Loading...</div>;

  return (
    <ul>
      {data?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Common Patterns

Use Both Builder and Serve

Keep direct query execution for jobs, use serve for APIs:

// lib/queries.ts - Export both
import { createQueryBuilder } from '@hypequery/clickhouse';
import { initServe } from '@hypequery/serve';

export const db = createQueryBuilder({ client });

// Direct execution for background jobs
export async function syncUsers() {
  return await db
    .table('users')
    .where('active', true)
    .execute();
}

// HTTP API for frontend
const { define, queries, query } = initServe({
  context: () => ({ db }),
});

export const api = define({
  queries: queries({
    activeUsers: query
      .query(async ({ ctx }) => {
        return ctx.db
          .table('users')
          .where('active', true)
          .execute();
      }),
  }),
});

Migrate Gradually

Don't migrate everything at once—start with critical endpoints:

// Phase 1: Keep existing code, add serve alongside
export async function getMetrics() {
  // Old way - still works!
  return await db.table('metrics').execute();
}

// Phase 2: Add serve for new APIs
const { define, queries, query } = initServe({
  context: () => ({ db }),
});

export const api = define({
  queries: queries({
    newEndpoint: query.query(async ({ ctx }) => {
      return ctx.db.table('new_data').execute();
    }),
  }),
});

// Phase 3: Migrate old endpoints when convenient
export const expandedApi = define({
  queries: queries({
    newEndpoint: query.query(async ({ ctx }) => {
      return ctx.db.table('new_data').execute();
    }),
    metrics: query.query(async ({ ctx }) => {
      // Migrated!
      return ctx.db.table('metrics').execute();
    }),
  }),
});

What's NOT Changing

  • Query syntax - Same .table(), .where(), .execute() methods
  • Type safety - Same TypeScript types and autocomplete
  • Schema - Same generated types work
  • Connection - Same db instance can be shared
  • Learning - Your existing query knowledge transfers

Next Steps

After migration:

  1. Add authenticationAuthentication Guide
  2. Enable multi-tenancyMulti-Tenancy Guide
  3. Set up React integrationReact Getting Started
  4. Configure cachingQuery Caching
  5. Add observabilityObservability Guide

Questions?

Can I use both packages together? Yes! See Use Both Builder and Serve above.

Do I need to rewrite my queries? No! The query builder API is 100% identical.

Can I migrate gradually? Yes—migrate your most important endpoints first, keep others as direct queries.

What if I need help?

On this page