Skip to main content
For the complete documentation index optimized for AI agents, see llms.txt.
Queries for HelixDB can also be authored in TypeScript using the @helix-db/helix-db package. The TypeScript DSL builds the same JSON AST as the Rust helix-db crate, so the resulting queries.json bundle is interchangeable. Use TypeScript when your service is already Node.js or you want end-to-end type inference on parameters. For the conceptual model and per-builder walkthrough, see the Querying Guide.

Prerequisites

  • Node.js 20 or later.
  • A package manager (npm, pnpm, or yarn).
  • Optional: the Helix CLI for deploying the resulting bundle.

Create a new project

mkdir helix-queries
cd helix-queries
npm init -y
npm pkg set type=module
@helix-db/helix-db is published as an ES module, so the "type": "module" field in package.json matters. Skip this step if you are adding queries to an existing ESM-configured project.

Add the dependency

npm install @helix-db/helix-db
npm install --save-dev typescript @types/node tsx
tsx is optional but convenient for running the generator without a separate build step.

Minimal tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src/**/*.ts"]
}
The DSL relies on strict mode for the typestate-tracked traversal builder; turn it on.

Author your queries

Queries are plain functions that return a ReadBatch or WriteBatch. To bundle them, declare their parameters with defineParams, wrap them with registerRead / registerWrite, and collect them with defineQueries. The bundle’s generate(path) method writes the same queries.json shape that the Rust generate_to_path emits.
src/queries.ts
import {
  NodeRef,
  Order,
  Predicate,
  PropertyInput,
  PropertyProjection,
  defineParams,
  defineQueries,
  g,
  param,
  readBatch,
  registerRead,
  registerWrite,
  writeBatch,
} from "@helix-db/helix-db";

// Parameters for the read route.
const userByUsernameParams = defineParams({
  username: param.string(),
});

function userByUsername(p = userByUsernameParams) {
  return readBatch()
    .varAs(
      "user",
      g()
        .nWithLabel("User")
        .where(Predicate.eqParam("username", "username"))
        .project([
          PropertyProjection.renamed("$id", "id"),
          PropertyProjection.new("username"),
          PropertyProjection.new("tier"),
        ]),
    )
    .returning(["user"]);
}

// Parameters for the write route.
const addUserParams = defineParams({
  username: param.string(),
  tier: param.string(),
});

function addUser(p = addUserParams) {
  return writeBatch()
    .varAs(
      "newUser",
      g()
        .addN("User", {
          username: PropertyInput.param("username"),
          tier: PropertyInput.param("tier"),
        })
        .project([PropertyProjection.renamed("$id", "id")]),
    )
    .returning(["newUser"]);
}

export const queries = defineQueries({
  read: {
    user_by_username: registerRead(userByUsername, userByUsernameParams),
  },
  write: {
    add_user: registerWrite(addUser, addUserParams),
  },
});
A few things to know about this snippet:
  • The keys under read and write name each query in the bundle and back the typed queries.call.* helpers (see below). Names must be unique across reads and writes — a duplicate throws GenerateError at bundle time.
  • PropertyInput.param("name") feeds a parameter into an addN property bag or a setProperty; for predicates use Predicate.eqParam(...) and friends.

Add a generator entry point

src/generate.ts
import { queries } from "./queries.js";

await queries.generate("queries.json");
console.log("generated queries.json");
queries.generate(path) writes the bundle and resolves to the path it wrote. The file is laid out identically to the Rust bundle so the runtime treats them as equivalent.

Wire up the script

{
  "scripts": {
    "generate": "tsx src/generate.ts"
  }
}
npm run generate
This produces queries.json in the project root. From here you can:

Sending registered queries from TypeScript

defineQueries also exposes a typed call map. Each entry takes the query’s parameter values (inferred from defineParams) and returns a DynamicQueryRequest you hand to the client’s .dynamic(...):
import { Client } from "@helix-db/helix-db";
import { queries } from "./queries.js";

const client = new Client("https://your-helix.example.com")
  .withApiKey("hx_secret"); // omit for a local instance

const { user } = await client
  .query<{ user: unknown }>()
  .dynamic(queries.call.user_by_username({ username: "alice" }))
  .send();
call.<name> is fully typed — an unknown key or wrong parameter type is a compile error — and sets query_name to the route key so logs show user_by_username, not __dynamic__. See Querying for the full client send path.

Next Steps

Querying Guide

Tutorial-style walkthrough of the DSL, from the simplest read to parameter-bound bundles.

Working with HelixDB

Deploy queries.json and the runtime workflow.

Local Development

Run the bundle locally with the enterprise-dev image.

npm package

@helix-db/helix-db on npm.