ClickHouse from TypeScript: TypeORM, Prisma, and what actually works

2026-11-12来源 dev.toEnglish

You've got a TypeScript backend on TypeORM or Prisma. You've added ClickHouse for analytics. The obvious next step is to point your ORM at it.

The short answer: you can't, and you shouldn't want to.

Neither TypeORM nor Prisma ships a ClickHouse driver. Even if one did, an ORM's model of the world is the wrong fit for an analytical database.

Here's why, and what TypeScript teams actually use instead. Disclosure: I maintain one of the options below, hypequery. I'll be clear about where the others are the better choice.

TypeORM, Prisma and Drizzle: where they stand

TypeORM. No ClickHouse driver. You'll find old GitHub issues asking for one and a few small community wrappers, but nothing maintained at the level you'd want in production.

Prisma. No ClickHouse connector. Prisma's $queryRaw only runs against databases Prisma already supports, so it can't reach ClickHouse either.

Drizzle. No first-party ClickHouse dialect at the time of writing. Drizzle is the closest in spirit (SQL-shaped, type-safe), which is why people ask for one.

The common pattern that works: keep your ORM for Postgres, and add a separate, ClickHouse-native layer for analytics. Two databases, two tools, each doing what it's good at. The question is which ClickHouse layer.

Option 1: the official client, @clickhouse/client
ClickHouse maintains an official Node.js client, written in TypeScript, with a separate @clickhouse/client-web build for browsers and edge runtimes. For a handful of queries it's all you need.
import { createClient } from '@clickhouse/client';

const client = createClient({ url: process.env.CLICKHOUSE_URL, username: 'default', password: process.env.CLICKHOUSE_PASSWORD, }); type RevenueRow = { region: string; revenue: string }; const rs = await client.query({ query: ` SELECT region, sum(amount) AS revenue FROM orders WHERE status = {status:String} GROUP BY region ORDER BY revenue DESC`, query_params: { status: 'completed' }, format: 'JSONEachRow', }); const rows = await rs.json<RevenueRow>(); 

It's fast, has no dependencies and supports parameterised queries. The catch is that RevenueRow is a promise you made to the compiler, not something it checked. Rename a column and TypeScript stays green while production breaks. Also, if amount is an integer column, revenue comes back as a string: ClickHouse serialises 64-bit integers as strings in JSON by default.

Use it when you have fewer than ten queries, or you need full control of the SQL.

Option 2: a typed query builder (hypequery)
This is the gap I built hypequery for: a ClickHouse-native query builder where the types come from your actual schema, not from interfaces you write by hand. Think Drizzle, but for an analytical database.

First, generate types from your live ClickHouse schema:

npm install @hypequery/clickhouse
npx hypequery-generate-types

Then query with full inference:

import { createQueryBuilder } from '@hypequery/clickhouse'; import type { IntrospectedSchema } from './generated-schema'; const db = createQueryBuilder<IntrospectedSchema>({ host: process.env.CLICKHOUSE_URL, username: 'default', password: process.env.CLICKHOUSE_PASSWORD, database: 'default', }); const revenueByRegion = await db .table('orders') .select(['region']) .where('status', 'eq', 'completed') .sum('amount', 'revenue') .groupBy('region') .orderBy('revenue', 'DESC') .execute(); 

The result type is inferred from the schema and the query. A typo in 'region' or a dropped column is a compile error, not a runtime surprise.

It wraps the official client underneath, so it works in Node and, with @hypequery/clickhouse plus @clickhouse/client-web, in browser and edge runtimes.

Beyond the query builder, the rest of the toolkit covers what analytics features usually need next: metric and dataset definitions, validated HTTP APIs with OpenAPI, typed React hooks, and multi-tenant scoping that fails closed. It's Apache-2.0 licensed.

Use it when you have more than a few queries, several people writing them, or a schema that changes. If you came looking for "TypeORM for ClickHouse", this is the closest equivalent. There's a comparison with ORM-style approaches here.

Connecting to ClickHouse Cloud from TypeScript

Whichever option you pick, a few ClickHouse Cloud details trip people up:
• Use the HTTPS endpoint. Cloud services expose HTTPS on port 8443, so your URL looks like https://..clickhouse.cloud:8443. Plain HTTP on 8123 won't connect.
• Idle services wake up slowly. If your service scales down when idle, the first query after a quiet period can take several seconds. Raise request_timeout so the first request after a quiet period doesn't time out.
• Edge runtimes need the web client. Vercel Edge and Cloudflare Workers don't have Node's http module. Use @clickhouse/client-web there.
• Give your app a read-only user. Create a dedicated user with readonly settings for the API path, rather than shipping the default credentials.
• Watch 64-bit numbers. UInt64, Int64 and Decimal results arrive as strings in JSON formats. Convert them deliberately rather than letting Number() silently lose precision.

What I'd do

Keep TypeORM or Prisma for your transactional data. Don't try to stretch it over ClickHouse.
For analytics, start with @clickhouse/client if you have a few queries and one person writing them.

Move to a typed query builder like hypequery once the queries multiply or the schema starts changing under you. That's usually the point where hand-written result types start lying.

Either way, treat ClickHouse as what it is: an analytical engine with its own rules, not a faster Postgres.

Using ClickHouse from TypeScript in a different way? I'd like to hear what's working for you in the comments.

原文:https://www.a8q9.cn/en/article/clickhouse-from-typescript-typeorm-prisma-and-what-actually-works-6047c4.html

相关文章