> hypequery

Node.js

Run hypequery as a standalone HTTP API using Hono

Node.js

Expose analytics through a standalone HTTP API using Hono. This guide shows you how to set up a production-ready Node.js server with type-safe ClickHouse queries.

Runnable Example

You can find a runnable Node.js example in our examples repo

Use case

You want a standalone Node.js API that:

  • Serves analytics queries over HTTP
  • Works with any frontend framework (React, Vue, Svelte, etc.)
  • Can be deployed independently (Docker, Railway, Fly.io, etc.)
  • Uses modern, lightweight framework (Hono) with great TypeScript support

Prerequisites

  • Node.js 18+
  • ClickHouse instance (local or hosted)
  • Sample data or your own ClickHouse tables

Install dependencies

# Core packages
npm install @hypequery/clickhouse @hypequery/serve zod

# Hono framework
npm install hono @hono/node-server dotenv

# CLI (required for type generation)
npm install -D @hypequery/cli

# TypeScript + dev tools
npm install -D typescript tsx

Why Hono? Hono is a modern, lightweight web framework with excellent TypeScript support. It's similar to Express but faster, more type-safe, and works great with edge runtimes. If you prefer Express or Fastify, hypequery works with those too - just adapt the server setup.

Configure environment variables

Create .env:

# .env
CLICKHOUSE_HOST=http://localhost:8123
CLICKHOUSE_USERNAME=default
CLICKHOUSE_PASSWORD=your_password
CLICKHOUSE_DATABASE=default

# Server configuration
PORT=3000

Important: Add .env to .gitignore.

Configure TypeScript

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "rootDir": "src",
    "outDir": "dist",
    "skipLibCheck": true
  }
}

Update package.json to add scripts:

{
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Generate TypeScript types

Generate types from your ClickHouse schema:

npx hypequery generate

What this does:

  • Connects to your ClickHouse instance
  • Introspects your table schemas
  • Generates analytics/schema.ts with TypeScript types

This file is auto-generated. Don't edit it manually - regenerate when your schema changes.

Create ClickHouse client

Create analytics/client.ts:

import { createQueryBuilder } from '@hypequery/clickhouse';
import type { IntrospectedSchema } from './schema';

export const db = createQueryBuilder<IntrospectedSchema>({
  host: process.env.CLICKHOUSE_HOST!,
  database: process.env.CLICKHOUSE_DATABASE!,
  username: process.env.CLICKHOUSE_USERNAME!,
  password: process.env.CLICKHOUSE_PASSWORD,
});

Note: Place this in analytics/ directory at project root (not src/).

Define analytics queries

Create analytics/queries.ts:

import { initServe } from '@hypequery/serve';
import { db } from './client';

const serve = initServe({
  basePath: '',
  context: () => ({ db }),
});
const { query } = serve;

export const api = serve.define({
  queries: serve.queries({
    tripsQuery: query
      .describe('Example query using the trips table')
      .query(async ({ ctx }) =>
        ctx.db
          .table('trips')
          .select('*')
          .limit(10)
          .execute()
      ),
  }),
});

// Register HTTP routes
api.route('/tripsQuery', api.queries.tripsQuery);

Key points:

  • Use ctx.db (not db) inside query functions
  • Always call .execute() at the end
  • Register routes with api.route() to expose via HTTP
  • Set basePath: '' to serve routes from root path

Create Hono app

Create src/app.ts:

import { Hono } from "hono";
import { api } from "../analytics/queries.js";
import { createFetchHandler } from "@hypequery/serve";

// Create Hono app
export const app = new Hono();

// Health check endpoint
app.get("/", (c) => {
  return c.json({
    status: "ok",
    runtime: "node",
  });
});

// Example: Direct query execution (server-side)
app.get("/trips", async (c) => {
  const result = await api.run('tripsQuery');
  return c.json(result);
});

// Wire hypequery handler into Hono's router
const hypequery = createFetchHandler(api.handler);
app.all('/tripsQuery', (c) => hypequery(c.req.raw));

What's happening:

  • Regular Hono routes like / and /trips work normally
  • The /tripsQuery route uses hypequery's HTTP handler
  • createFetchHandler adapts hypequery to Hono's Request/Response API

Create server entry point

Create src/index.ts:

import "dotenv/config";
import { serve } from "@hono/node-server";
import { app } from "./app.js";

const port = process.env.PORT
  ? Number(process.env.PORT)
  : 3000;

serve({
  fetch: app.fetch,
  port,
});

console.log(`🚀 Server running on http://localhost:${port}`);

Run the development server

npm run dev

Your API is now available at http://localhost:3000.

Test the endpoints:

# Health check
curl http://localhost:3000/

# Direct query execution (server-side)
curl http://localhost:3000/trips

# HTTP query endpoint
curl http://localhost:3000/tripsQuery

Optional: Run hypequery dev server for interactive docs

npx hypequery dev analytics/queries.ts
# Docs at: http://localhost:4000/docs

Project Structure

After following this guide, your structure should look like:

schema.ts (Auto-generated by `hypequery generate`)
client.ts (You write this)
queries.ts (You write this)
.env (You create this)
package.json
tsconfig.json

Testing Your API

# Test health check
curl http://localhost:3000/

# Test trips endpoint (direct execution)
curl http://localhost:3000/trips

# Test hypequery HTTP endpoint
curl http://localhost:3000/tripsQuery

# View OpenAPI spec (if using hypequery dev)
curl http://localhost:4000/openapi.json

When to use which approach

You have two options for serving queries:

1. Direct execution with api.run() (like /trips endpoint):

  • Executes queries in-process
  • Returns raw results without HTTP overhead
  • Great for server-side rendering or internal APIs

2. HTTP handler with createFetchHandler() (like /tripsQuery endpoint):

  • Exposes queries via HTTP with automatic validation
  • Generates OpenAPI documentation
  • Great for external APIs and frontend consumption

Both approaches use the same query definitions, choose based on your use case.

On this page