---
title: Embed relate in a Node.js app
description: "relate, the embedded Cypher graph database (relatedb.ai): A first Node.js program on the @relate/relate package, which opens a store, passes parameters, runs transactions, reads result metadata, and catches errors."
type: tutorial
surfaces:
  - node
status: stable
verified: 0.1.2
verified_by:
  - node
samples:
  - https://relatedb.ai/docs/get-started/node/samples/version.json
  - https://relatedb.ai/docs/get-started/node/samples/open.json
  - https://relatedb.ai/docs/get-started/node/samples/create.json
  - https://relatedb.ai/docs/get-started/node/samples/params.json
  - https://relatedb.ai/docs/get-started/node/samples/transaction.json
  - https://relatedb.ai/docs/get-started/node/samples/metadata.json
  - https://relatedb.ai/docs/get-started/node/samples/error.json
  - https://relatedb.ai/docs/get-started/node/samples/program.json
canonical_url: https://relatedb.ai/docs/get-started/node/
prerequisites: []
related:
  - get-started/python
  - get-started/first-graph
  - reference/errors
  - concepts/transactions-and-concurrency
  - reference/storage/formats
  - guides/integrations/vercel-ai-sdk
  - get-started/rust
  - get-started/java
  - get-started/go
  - get-started/swift
  - get-started/browser
  - guides/embed/node
  - reference/api/javascript
last_updated: 2026-11-08
---

# Embed relate in a Node.js app

A first Node.js program on the @relate/relate package, which opens a store, passes parameters, runs transactions, reads result metadata, and catches errors.

> Verified against relate 0.1.2. If your `relate --version` is newer, read https://relatedb.ai/docs/reference/changelog/#since-0.1.2

By the end of this page, you'll have a Node.js program that keeps a small graph of people and projects in one file. It reads the graph with parameters and reports errors by their class and code. Along the way we write inside a [transaction](https://relatedb.ai/docs/reference/glossary/#transaction), roll one back, and read what a result says about itself. It takes about fifteen minutes. relate runs inside your Node.js process, so there is no server to start, and every step writes to a store it creates.

## Prerequisites

- Node.js 18 or later, with `npm`.
- A terminal and a text editor. The relate CLI is not needed: the package carries the engine.

## Install the client

The package is `@relate/relate`. Install it in your project's directory:

```bash
npm install @relate/relate
```

npm adds the engine as a platform package for your operating system and CPU (`@relate/<platform>-<arch>`). Check that Node.js can load it, and which version it is:

```js run name=version
const { Database } = require("@relate/relate");
const { version } = require("@relate/relate/package.json");

Database.open(":memory:");
console.log(version);
```

```text
0.1.2
```

Requiring `@relate/relate` loads the engine, and opening a `:memory:` store runs it, so a missing or mismatched platform package fails at this step.

Notice the version: the programs on this page were checked against that release, and their recorded output comes from it.

## Open a store

A [store](https://relatedb.ai/docs/reference/glossary/#store) is where relate keeps a graph. `:memory:` is a store that lives in the process and forgets everything when the process exits. A path ending in `.rl` is a single-file store, which `Database.open()` creates when nothing is there yet.

```js run name=open
const fs = require("node:fs");
const { Database } = require("@relate/relate");

const memory = Database.open(":memory:");
const conn = memory.connect();
console.log(conn.query("RETURN 'hello' AS greeting").single().greeting);

Database.open("./team.rl");
console.log(fs.existsSync("team.rl"));
```

```text
hello
true
```

`Database.open()` opens a store, `connect()` returns a connection on it, and `query()` runs one statement and returns its result. Notice that `team.rl` now exists. `query()` is synchronous: it returns the result, not a Promise. `queryAsync()` takes the same arguments and returns a Promise of the same result, running the statement off the main thread.

## Create the graph

Our graph is three people and two projects: who works on what, since when, and who maintains `relate`. One `CREATE` statement writes all of it. The rest of this page uses the same statement, as `TEAM`, on an in-memory store, so each program runs on its own.

```js run name=create
const { Database } = require("@relate/relate");

const TEAM = `
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace)
`;

const conn = Database.open(":memory:").connect();
const result = conn.query(TEAM);
console.log("nodes created:", result.stats.nodes_created);
console.log("relationships created:", result.stats.relationships_created);
```

```text
nodes created: 5
relationships created: 5
```

A write returns no rows, so we read its counters from `result.stats`. Five nodes and five relationships: the whole team.

## Read it back with parameters

A statement names each value it needs as `$name`, and `query()` takes the values as a plain object. The statement text stays the same while the values change, and a value never becomes part of the Cypher.

```js run name=params
const { Database } = require("@relate/relate");

const TEAM = `
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace)
`;

const TEAM_OF = `
MATCH (p:Person)-[w:WORKS_ON]->(:Project {name: $project})
RETURN p.name AS name, w.since AS since
ORDER BY since
`;

const conn = Database.open(":memory:").connect();
conn.query(TEAM);
for (const project of ["relate", "opencypher-tck"]) {
  console.log(project);
  for (const row of conn.query(TEAM_OF, { project }).rows) {
    console.log(" ", row.name, "since", row.since);
  }
}
```

```text
relate
  Grace since 2023
  Ada since 2024
opencypher-tck
  Grace since 2022
```

`result.rows` is an array of objects keyed by the names in `RETURN`. Notice that Noor isn't listed: she maintains `relate` through a `MAINTAINS` relationship, and the pattern asks for `WORKS_ON`.

## Change it in a transaction

A connection commits each statement on its own until you call `begin()`. From then on, every statement is part of one transaction, `commit()` keeps it, and `rollback()` throws it away. This program adds Lin to the team and commits, then deletes every project and rolls back.

```js run name=transaction
const { Database } = require("@relate/relate");

const TEAM = `
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace)
`;

const COUNT_PEOPLE = "MATCH (p:Person) RETURN count(p) AS n";
const COUNT_PROJECTS = "MATCH (p:Project) RETURN count(p) AS n";

const conn = Database.open(":memory:").connect();
conn.query(TEAM);

conn.begin();
conn.query(
  "MATCH (tck:Project {name: 'opencypher-tck'}) " +
    "CREATE (:Person {name: 'Lin', role: 'engineer'})-[:WORKS_ON {since: 2026}]->(tck)"
);
conn.commit();
console.log("people after commit:", conn.query(COUNT_PEOPLE).single().n);

conn.begin();
conn.query("MATCH (p:Project) DETACH DELETE p");
console.log("projects inside the transaction:", conn.query(COUNT_PROJECTS).single().n);
conn.rollback();
console.log("projects after rollback:", conn.query(COUNT_PROJECTS).single().n);
```

```text
people after commit: 4
projects inside the transaction: 0
projects after rollback: 2
```

Inside the transaction, the delete is visible: zero projects. After `rollback()`, both are back, and Lin, who was committed, stays. Each transaction starts with its own `begin()`.

## Read result metadata

A result describes itself as well as holding rows: the column names, what kind of statement it was, and a summary of what it changed.

```js run name=metadata
const { Database } = require("@relate/relate");

const TEAM = `
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace)
`;

const conn = Database.open(":memory:").connect();
conn.query(TEAM);

const result = conn.query(
  "MATCH (p:Person)-[w:WORKS_ON]->(:Project {name: $project}) " +
    "RETURN p.name AS name, w.since AS since ORDER BY since",
  { project: "relate" }
);
console.log(result.columns);
for (const [column, value] of Object.entries(result.rows[0])) {
  console.log(column, typeof value);
}
console.log("query type:", result.queryType);

const write = conn.query("MATCH (n:Person {name: 'Noor'}) SET n.role = 'lead'");
const summary = write.consume();
console.log("query type:", summary.queryType);
console.log("properties set:", summary.counters.properties_set);
```

```text
[ 'name', 'since' ]
name string
since number
query type: r
query type: w
properties set: 1
```

A result has no list of column types: each value carries its own. A Cypher string arrives as a `string`, an integer as a `number` (a `bigint` beyond `Number.MAX_SAFE_INTEGER`, so nothing is rounded), and a node as an object with `kind: "node"`. `queryType` is `r` for a read and `w` for a write that returns no columns, and `consume()` returns the summary, whose `counters` hold what the statement changed.

## Catch an error

Every error the engine reports throws a `RelateError`, carrying the engine's classification of what went wrong. A misuse caught in JavaScript throws a `TypeError` or a `ResultNotSingleError` instead, so the programs on this page rethrow anything that isn't a `RelateError`. A statement that uses `$name` without a value is rejected before it runs:

```js run name=error
const { Database, RelateError } = require("@relate/relate");

const TEAM = `
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace)
`;

const ROLE_OF = "MATCH (p:Person {name: $name}) RETURN p.role AS role";

const conn = Database.open(":memory:").connect();
conn.query(TEAM);

try {
  conn.query(ROLE_OF);
} catch (e) {
  if (!(e instanceof RelateError)) throw e;
  console.log(e.name);
  console.log(e.message);
  console.log("gqlStatus:", e.gqlStatus);
  console.log("code:", e.code);
  console.log("retryable:", e.retryable);
}

console.log(conn.query(ROLE_OF, { name: "Noor" }).single().role);
```

```text
RelateError
semantic error: ParameterMissing: Expected parameter(s): name
gqlStatus: 42N02
code: Neo.ClientError.Statement.ParameterMissing
retryable: false
maintainer
```

The message says what's missing; the properties are what code branches on:

| Property    | This error                                   | Meaning                                                                                                       |
| ----------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `gqlStatus` | `42N02`                                      | The GQLSTATUS code; the Python client reports the same code for the same error                                |
| `code`      | `Neo.ClientError.Statement.ParameterMissing` | The Neo4j status code for the same error                                                                      |
| `retryable` | `false`                                      | Whether running it again can succeed; `true` for a write-write conflict, never for a mistake in the statement |

The [errors reference](https://relatedb.ai/docs/reference/errors/#engine-parameter-missing) lists this message with its cause and remedy. With the value bound, the same statement finds Noor's role.

## Put it together

`team.js` puts the steps together in one program. It opens `team.rl`, creates the graph only when the store has no people yet, and prints who works on each project. A relate error anywhere ends it with the error's code and a nonzero exit status.

```js run name=program title="team.js"
const { Database, RelateError } = require("@relate/relate");

const TEAM = `
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace)
`;

const TEAM_OF = `
MATCH (p:Person)-[w:WORKS_ON]->(:Project {name: $project})
RETURN p.name AS name, w.since AS since
ORDER BY since
`;

function ensureTeam(conn) {
  const people = conn.query("MATCH (p:Person) RETURN count(p) AS n").single().n;
  if (people === 0) {
    conn.query(TEAM);
    console.log("created the team graph");
  } else {
    console.log("found", people, "people");
  }
}

function report(conn, project) {
  console.log(project);
  for (const row of conn.query(TEAM_OF, { project }).rows) {
    console.log(" ", row.name, "since", row.since);
  }
}

function main() {
  const conn = Database.open("./team.rl").connect();
  ensureTeam(conn);
  report(conn, "relate");
  report(conn, "opencypher-tck");
}

try {
  main();
} catch (e) {
  if (!(e instanceof RelateError)) throw e;
  console.error(`relate error ${e.gqlStatus}: ${e.message}`);
  process.exitCode = 1;
}
```

```text
created the team graph
relate
  Grace since 2023
  Ada since 2024
opencypher-tck
  Grace since 2022
```

Run it with `node team.js`. The client has no close method: each statement is durable in `team.rl` when `query()` returns, so the program ends without closing anything. Run it a second time and it prints `found 3 people` instead of creating the graph again, then the same report. If another process has `team.rl` open for writing, `Database.open()` throws a `RelateError`, and the program prints it and exits with status 1.

To read the file with no chance of changing it, open it [read-only](https://relatedb.ai/docs/reference/glossary/#read-only): `Database.open("./team.rl", { readOnly: true })`. A write then throws a `RelateError` with `gqlStatus` `42N18`, and a path with no store behind it throws instead of creating a new store.

## What you built

You have a Node.js program that keeps a graph of people and projects in `team.rl`, one file you can copy or archive. It reads with parameters, writes inside transactions, knows a read from a write by its query type, and reports errors by class and GQLSTATUS code. Ada, Grace, and Noor are waiting in that file for the next run.

## Next

- [Embed relate in a Python app](https://relatedb.ai/docs/get-started/python/): the same steps, the same graph, in Python.
- [Use the Node.js client](https://relatedb.ai/docs/guides/embed/node/): open modes, typed values, tagged templates, async queries and errors beyond the first program.
- [Run relate in the browser](https://relatedb.ai/docs/get-started/browser/): the same graph in a web page, on the WebAssembly build.
- [Errors](https://relatedb.ai/docs/reference/errors/): every error message, its cause, its remedy, and its codes.
- [Transactions and concurrency](https://relatedb.ai/docs/concepts/transactions-and-concurrency/): what a transaction isolates, and why a store has one writer.
- [Use relate with the Vercel AI SDK](https://relatedb.ai/docs/guides/integrations/vercel-ai-sdk/): the same client behind a read-only agent tool.
- [Your first graph in the CLI](https://relatedb.ai/docs/get-started/first-graph/): the same graph from the command line.
- [Storage formats](https://relatedb.ai/docs/reference/storage/formats/): the three store layouts and what each writes to disk.

## Terms used on this page

- **transaction**: A unit of work that commits as a whole or not at all; each statement is its own transaction unless an explicit `begin` … `commit` groups several.
- **store**: A relate database: a directory, a single `.rl` file, or `:memory:`, opened with `--database`.
- **read-only**: An open mode (`--read-only`) that never writes, checkpoints or migrates the store; writes fail, and a directory store can be read beside its writer.
