neo4j-driver-javascript-skill

SkillDev tools

Neo4j JavaScript/TypeScript Driver v6 — driver lifecycle, executeQuery,

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the neo4j-driver-javascript-skill skill

What this skill tells your AI

The instructions your AI receives, as published by neo4j-contrib/neo4j-skills in neo4j-driver-javascript-skill/SKILL.md and read by ahel’s review.

When to Use

  • Writing JS/TS code that connects to Neo4j (Node.js or browser)
  • Setting up driver, sessions, transactions, or query execution
  • Debugging Integer handling, result consumption, session leaks, async errors
  • TypeScript type annotations for driver objects

When NOT to Use

  • Writing/optimizing Cypherneo4j-cypher-skill
  • Upgrading driver versionneo4j-migration-skill
  • RxJS session APIreferences/rxjs-session.md

Install

npm install neo4j-driver   # or: yarn add neo4j-driver

Environment Variables

Load connection config from environment — never hardcode credentials.

# .env file (add to .gitignore)
NEO4J_URI=neo4j+s://xxx.databases.neo4j.io
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=secret
NEO4J_DATABASE=neo4j
// npm install dotenv (for Node.js < 20 or when .env auto-load is off)
import 'dotenv/config'   // or: require('dotenv').config()

const URI      = process.env.NEO4J_URI
const USER     = process.env.NEO4J_USERNAME
const PASSWORD = process.env.NEO4J_PASSWORD
const DATABASE = process.env.NEO4J_DATABASE ?? 'neo4j'

Node 20+ natively loads .env with --env-file .env. Next.js / Vite auto-load .env — no dotenv import needed.


Driver Lifecycle

Create one driver instance at startup. Share everywhere. Never create per-request.

// CommonJS
const neo4j = require('neo4j-driver')
// ESM / TypeScript
import neo4j from 'neo4j-driver'

const driver = neo4j.driver(
  process.env.NEO4J_URI,                              // 'neo4j+s://xxx.databases.neo4j.io'
  neo4j.auth.basic(process.env.NEO4J_USER, process.env.NEO4J_PASSWORD)
)
await driver.verifyConnectivity()   // fail fast on startup if unreachable
// On shutdown:
await driver.close()

URI schemes:

SchemeTransportUse
neo4j+s://TLS + cluster routingAura; production clusters
neo4j://plaintext + cluster routinglocal dev cluster
bolt+s://TLS, single instancesingle Neo4j instance with TLS
bolt://plaintext, single instancelocal single instance

Auth options:

neo4j.auth.basic(user, password)   // username/password
neo4j.auth.bearer(token)           // SSO / JWT
neo4j.auth.kerberos(base64Ticket)  // Kerberos
neo4j.auth.none()                  // unauthenticated (dev only)

Singleton for web frameworks — create once, import everywhere:

// db.js
let _driver = null
export function getDriver() {
  if (!_driver) _driver = neo4j.driver(process.env.NEO4J_URI,
    neo4j.auth.basic(process.env.NEO4J_USER, process.env.NEO4J_PASSWORD))
  return _driver
}
export async function closeDriver() {
  if (_driver) { await _driver.close(); _driver = null }
}

Serverless (Lambda/Vercel/Workers): keep maxConnectionPoolSize: 5; no guaranteed SIGTERM.


Choose the Right API

APIUse whenAuto-retryResult
driver.executeQuery()Default for most querieseager (all records)
session.executeRead/Write()Large results, streaming, multi-query txlazy stream
session.run()LOAD CSV, CALL IN TRANSACTIONS, scriptslazy stream

executeQuery — Default

const { records, summary, keys } = await driver.executeQuery(
  'MATCH (p:Person {name: $name})-[:KNOWS]->(f) RETURN f.name AS name',
  { name: 'Alice' },
  { database: 'neo4j', routing: neo4j.routing.READ }
)
for (const record of records) {
  console.log(record.get('name'))   // use .get() — records are NOT plain objects
}

// Write and count results
const { summary: s } = await driver.executeQuery(
  'CREATE (p:Person {name: $name, age: $age})',
  { name: 'Bob', age: neo4j.int(30) },
  { database: 'neo4j' }
)
console.log(s.counters.updates().nodesCreated)   // ✅ must call .updates()

Always specify database — omitting causes an extra round-trip.

❌ Never template-literal Cypher:

// ❌ injection risk + disables plan caching
await driver.executeQuery(`MATCH (p:Person {name: '${name}'}) RETURN p`)
// ✅ parameterised
await driver.executeQuery('MATCH (p:Person {name: $name}) RETURN p', { name })

Managed Transactions (executeRead / executeWrite)

Auto-retried on transient failures. Consume records inside the callback — the stream is gone when the callback returns.

const session = driver.session({ database: 'neo4j' })
try {
  const names = await session.executeRead(async tx => {
    const result = await tx.run(
      'MATCH (p:Person) WHERE p.name STARTS WITH $prefix RETURN p.name AS name',
      { prefix: 'Al' }
    )
    // ✅ collect() while tx is open; return plain data
    return (await result.collect()).map(r => r.get('name'))
  })

  await session.executeWrite(async tx => {
    await tx.run('MERGE (p:Person {name: $name})', { name: 'Carol' })
  })
} finally {
  await session.close()   // always in finally
}

Critical: await tx.run() returns a stream handle, not records.

// ❌ returns stream; tx closes; records = []
return await tx.run('MATCH (p:Person) RETURN p.name AS name')

// ✅ collect fully inside callback
const result = await tx.run('MATCH (p:Person) RETURN p.name AS name')
return (await result.collect()).map(r => r.get('name'))

// ✅ or stream with for-await
for await (const record of result) { names.push(record.get('name')) }

Callback may execute more than once (retry on transient failure) — no side effects inside:

// ❌ fetch() called on every retry
await session.executeWrite(async tx => {
  await fetch('https://api.example.com/notify')
  await tx.run('CREATE ...')
})
// ✅ side effects after confirmed commit
await session.executeWrite(async tx => { await tx.run('MERGE ...') })
await fetch('https://api.example.com/notify')

session.run — Implicit Transactions

Not auto-retried. Use for LOAD CSV / CALL IN TRANSACTIONS / scripting only.

const session = driver.session({ database: 'neo4j' })
try {
  const result = await session.run('CREATE (p:Person {name: $name}) RETURN p', { name: 'Alice' })
  console.log(result.summary.counters.updates().nodesCreated)
} finally {
  await session.close()
}

Session Close — Always in finally

// ❌ session leaks if executeRead rejects
session.executeRead(async tx => { ... })
  .then(result => doSomething(result))
  .then(() => session.close())

// ✅ guaranteed close
try {
  const result = await session.executeRead(async tx => { ... })
  doSomething(result)
} finally {
  await session.close()
}

// ✅ promise-chain equivalent
session.executeRead(async tx => { ... })
  .then(doSomething)
  .catch(handleError)
  .finally(() => session.close())

Integer Handling

Neo4j integers are 64-bit; JS Number is IEEE 754 (safe up to 2^53−1). Driver returns custom Integer by default.

Three modes:

// Mode 1 (default): Integer class — safe for all values, requires conversion
const driver1 = neo4j.driver(URI, auth)
// record.get('count') → Integer { low: 42, high: 0 }

// Mode 2: native JS number — only safe within Number.MAX_SAFE_INTEGER
const driver2 = neo4j.driver(URI, auth, { disableLosslessIntegers: true })
// record.get('count') → 42

// Mode 3: BigInt — precise but breaks JSON.stringify
const driver3 = neo4j.driver(URI, auth, { useBigInt: true })
// record.get('count') → 42n

Working with Integer class:

const count = record.get('count')        // Integer { low: 42, high: 0 }
neo4j.isInt(count)                       // true
neo4j.integer.inSafeRange(count)         // check before toNumber()
count.toNumber()                         // 42  (only safe within MAX_SAFE_INTEGER)
count.toString()                         // '42' (always safe)
count.toBigInt()                         // 42n

// Send integer parameter — plain JS number sends as FLOAT
await driver.executeQuery('CREATE (p:Person {age: $age})', { age: neo4j.int(30) })

JSON serialization pitfalls:

// ❌ Integer → {"low":42,"high":0}
JSON.stringify({ age: record.get('age') })
// ❌ BigInt → TypeError: Do not know how to serialize a BigInt
// ✅ convert first
JSON.stringify({ age: record.get('age').toNumber() })
// ✅ or use disableLosslessIntegers: true
// ❌ temporal types → {} silently
JSON.stringify({ dt: record.get('created') })
// ✅
JSON.stringify({ dt: record.get('created').toString() })

Record Access

Records are not plain objects — use .get():

const record = records[0]
record.get('name')    // ✅ by key
record.get(0)         // ✅ by index
record.keys           // ['name', 'age']
record.has('name')    // true
record.name           // ❌ undefined
record['name']        // ❌ undefined

record.toObject() returns plain JS keys but values are still driver types (Integers, temporals). Not JSON-safe without conversion.


Error Handling

import { Neo4jError, SERVICE_UNAVAILABLE, SESSION_EXPIRED } from 'neo4j-driver'

try {
  await driver.executeQuery('...', {}, { database: 'neo4j' })
} catch (err) {
  if (err instanceof Neo4jError) {
    if (err.code === 'Neo.ClientError.Schema.ConstraintValidationFailed') { /* unique constraint */ }
    if (err.code === SERVICE_UNAVAILABLE) { /* unreachable */ }
    if (err.code === SESSION_EXPIRED)     { /* open a new session */ }
    if (err.retriable) { /* transient — executeQuery already retried to exhaustion */ }
  }
}

executeQuery and executeRead/Write auto-retry retriable errors. session.run does not.


TypeScript

import neo4j, { Driver, Session, ManagedTransaction, Record, Node, Integer } from 'neo4j-driver'

const driver: Driver = neo4j.driver(URI, neo4j.auth.basic(USER, PASSWORD))
const session: Session = driver.session({ database: 'neo4j' })

const names: string[] = await session.executeRead(
  async (tx: ManagedTransaction): Promise<string[]> => {
    const result = await tx.run('MATCH (p:Person) RETURN p.name AS name')
    return (await result.collect()).map((r: Record) => r.get('name') as string)
  }
)

// Typed node — Integer generic changes with disableLosslessIntegers
const node = record.get('p') as Node<Integer>
const age: number = node.properties.age.toNumber()
// With disableLosslessIntegers: true → Node<number>; age is already number

Batch Writes with UNWIND

// ❌ one transaction per item
for (const p of people) { await driver.executeQuery('CREATE ...', p) }

// ✅ single transaction
await driver.executeQuery(
  `UNWIND $people AS person
   MERGE (p:Person {name: person.name})
   SET p.age = person.age`,
  { people },   // array of plain objects; numeric fields must be JS numbers, not neo4j.int()
  { database: 'neo4j' }
)

Common Mistakes

MistakeFix
Template literal CypherUse $param placeholders
record.name / record['name']record.get('name')
JSON.stringify on Integer.toNumber() or disableLosslessIntegers
JSON.stringify on temporal.toString() first
summary.counters.nodesCreatedsummary.counters.updates().nodesCreated
Omit databaseAlways { database: 'neo4j' }
Return result from tx callbackReturn await result.collect() or mapped data
.then(() => session.close())try/finally { await session.close() }
Side effects inside tx callbackMove outside — callback may retry
New driver per requestCreate once at startup
bolt:// or neo4j:// in browserUse neo4j+s:// (WSS)
Integer in UNWIND arrayConvert to plain JS Number first
maxConnectionPoolSize: 100 in serverlessUse 5–10 per function instance

References

Load on demand:


Checklist

  • One driver instance created at startup; shared everywhere
  • database specified on every query/session
  • await driver.verifyConnectivity() called at startup
  • session.close() in finally block
  • Records accessed with .get() — not dot/bracket notation
  • Integer .toNumber() / .toString() called before JSON serialization
  • Temporal .toString() called before JSON serialization
  • summary.counters.updates() called before accessing counter fields
  • $param placeholders used — no string concatenation in Cypher
  • Tx callback returns plain data (not stream); side effects outside callback
  • Write uses neo4j.int() for integer parameters
  • executeRead for reads; executeWrite for writes
  • Browser: neo4j+s:// URI (WebSocket)

Signals

GitHub stars
110
Forks
38
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
neo4j-driver-javascript-skill
Source
github.com/neo4j-contrib/neo4j-skills