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 tsxWhy 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=3000Important: 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 generateWhat this does:
- Connects to your ClickHouse instance
- Introspects your table schemas
- Generates
analytics/schema.tswith 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(notdb) 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/tripswork normally - The
/tripsQueryroute uses hypequery's HTTP handler createFetchHandleradapts 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 devYour 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/tripsQueryOptional: Run hypequery dev server for interactive docs
npx hypequery dev analytics/queries.ts
# Docs at: http://localhost:4000/docsProject Structure
After following this guide, your structure should look like:
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.jsonWhen 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.