neo4j-driver-js

SKILL.md

Neo4j JavaScript Driver

When to Use

Use this skill when working with the neo4j-driver package in JavaScript/TypeScript. Covers driver initialization, session management, the impersonation security model, transaction functions with retries, Neo4j type handling (Integer, DateTime, Node, Relationship), and mapping driver results to UI-consumable shapes.


1. Installation

npm install neo4j-driver

2. Driver Initialization

Create a single driver instance for the application lifetime. The driver manages a connection pool internally.

import neo4j, { Driver } from "neo4j-driver";

let driver: Driver | null = null;

export const initDriver = (
  uri: string,
  user: string,
  password: string,
): Driver => {
  driver = neo4j.driver(uri, neo4j.auth.basic(user, password), {
    maxConnectionPoolSize: 50,
    connectionAcquisitionTimeout: 60000, // 60s
    maxTransactionRetryTime: 30000, // 30s
  });
  return driver;
};

export const getDriver = (): Driver => {
  if (!driver)
    throw new Error("Driver not initialized. Call initDriver() first.");
  return driver;
};

export const closeDriver = async (): Promise<void> => {
  if (driver) {
    await driver.close();
    driver = null;
  }
};

Environment Configuration

export const NEO4J_CONFIG = {
  uri: import.meta.env.VITE_NEO4J_URI || "neo4j://localhost:7687",
  user: import.meta.env.VITE_NEO4J_USER || "neo4j",
  password: import.meta.env.VITE_NEO4J_PASSWORD || "password",
  database: import.meta.env.VITE_NEO4J_DATABASE || "neo4j",
};

Verify Connectivity

const driver = initDriver(config.uri, config.user, config.password);
await driver.verifyConnectivity();
console.log("Connected to Neo4j");

3. Session Management

Sessions are lightweight and should be created per unit of work, then closed.

import { Session } from "neo4j-driver";

export const getSession = (mode: "READ" | "WRITE" = "READ"): Session => {
  return getDriver().session({
    database: NEO4J_CONFIG.database,
    defaultAccessMode:
      mode === "READ" ? neo4j.session.READ : neo4j.session.WRITE,
  });
};

Session Helper (Auto-Close)

export const withSession = async <T>(
  fn: (session: Session) => Promise<T>,
  mode: "READ" | "WRITE" = "READ",
): Promise<T> => {
  const session = getSession(mode);
  try {
    return await fn(session);
  } finally {
    await session.close();
  }
};

4. Impersonation (Two-Tier Security)

The application connects as a service account and impersonates the actual user. Neo4j RBAC enforces data-level permissions for the impersonated user.

export const getImpersonatedSession = (userEmail: string): Session => {
  return getDriver().session({
    database: NEO4J_CONFIG.database,
    impersonatedUser: userEmail, // Executes as this user's Neo4j RBAC role
  });
};

export const withImpersonation = async <T>(
  userEmail: string,
  fn: (session: Session) => Promise<T>,
): Promise<T> => {
  const session = getImpersonatedSession(userEmail);
  try {
    return await fn(session);
  } finally {
    await session.close();
  }
};

Usage

// User only sees data their Neo4j role permits
const transactions = await withImpersonation(
  "john.smith@company.com",
  async (session) => {
    const result = await session.run(
      `MATCH (t:Transaction)
       WHERE t.timestamp >= $startDate
       RETURN t ORDER BY t.timestamp DESC LIMIT 100`,
      { startDate },
    );
    return result.records.map((r) => r.get("t").properties);
  },
);

Prerequisites

  • Neo4j Enterprise Edition 5.x
  • Service account with GRANT IMPERSONATE privilege
  • Users created in Neo4j mapped to IdP identities

5. Transaction Functions

Prefer executeRead / executeWrite over raw session.run(). They provide automatic retries for transient errors (network blips, leader changes).

// Read transaction with automatic retries
const persons = await withSession(async (session) => {
  return session.executeRead(async (tx) => {
    const result = await tx.run(
      `MATCH (p:Person {id: $personId})-[:OWNS]->(a:Account)
       RETURN p, collect(a) AS accounts`,
      { personId },
    );
    return result.records.map((record) => ({
      person: record.get("p").properties,
      accounts: record.get("accounts").map((a: any) => a.properties),
    }));
  });
});

// Write transaction with automatic retries
await withSession(async (session) => {
  return session.executeWrite(async (tx) => {
    await tx.run(`CREATE (p:Person $props)`, {
      props: { id: crypto.randomUUID(), name: "Jane Doe" },
    });
  });
}, "WRITE");

6. Neo4j Type Handling

The driver returns Neo4j-specific types that must be converted before use in UI code.

Integer

Neo4j integers are 64-bit. The driver wraps them in neo4j.Integer objects.

import neo4j from "neo4j-driver";

// Check if a value is a Neo4j Integer
if (neo4j.isInt(value)) {
  const jsNumber = value.toNumber(); // Safe for values < Number.MAX_SAFE_INTEGER
  const jsString = value.toString(); // Safe for any value
  const jsBigInt = value.toBigInt(); // Native BigInt
}

DateTime

if (neo4j.isDateTime(value)) {
  const isoString = value.toString(); // ISO 8601 string
  const jsDate = value.toStandardDate(); // JavaScript Date object
}

if (neo4j.isDate(value)) {
  const isoString = value.toString(); // YYYY-MM-DD
}

if (neo4j.isDuration(value)) {
  const str = value.toString(); // ISO 8601 duration
}

Node, Relationship, Path

// Node
record.get("n").identity; // neo4j.Integer (internal ID)
record.get("n").elementId; // string (stable element ID — use this)
record.get("n").labels; // string[]
record.get("n").properties; // Record<string, any>

// Relationship
record.get("r").identity; // neo4j.Integer
record.get("r").elementId; // string
record.get("r").type; // string (relationship type)
record.get("r").startNodeElementId; // string
record.get("r").endNodeElementId; // string
record.get("r").properties; // Record<string, any>

// Path
record.get("path").start; // Node
record.get("path").end; // Node
record.get("path").segments; // Array<{ start, relationship, end }>
record.get("path").length; // number

7. Result-to-UI Data Mapping

Convert Neo4j driver results into shapes that Zustand stores and NVL components expect.

Property Mapping (Handle Neo4j Types)

function mapProperties(
  props: Record<string, unknown>,
): Record<string, unknown> {
  const mapped: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(props)) {
    if (neo4j.isInt(value)) {
      mapped[key] = value.toNumber();
    } else if (neo4j.isDateTime(value) || neo4j.isDate(value)) {
      mapped[key] = value.toString();
    } else if (neo4j.isDuration(value)) {
      mapped[key] = value.toString();
    } else {
      mapped[key] = value;
    }
  }
  return mapped;
}

Neo4j Node → Application GraphNode

interface GraphNode {
  id: string;
  labels: string[];
  properties: Record<string, unknown>;
}

function toGraphNode(neo4jNode: any): GraphNode {
  return {
    id: neo4jNode.elementId,
    labels: neo4jNode.labels,
    properties: mapProperties(neo4jNode.properties),
  };
}

Neo4j Relationship → Application GraphRelationship

interface GraphRelationship {
  id: string;
  type: string;
  source: string;
  target: string;
  properties: Record<string, unknown>;
}

function toGraphRelationship(neo4jRel: any): GraphRelationship {
  return {
    id: neo4jRel.elementId,
    type: neo4jRel.type,
    source: neo4jRel.startNodeElementId,
    target: neo4jRel.endNodeElementId,
    properties: mapProperties(neo4jRel.properties),
  };
}

Full Query → Store Data

export const expandNetwork = async (personId: string, hops: number = 2) => {
  return withSession(async (session) => {
    const result = await session.executeRead(async (tx) => {
      return tx.run(
        `MATCH path = (p:Person {id: $personId})-[*1..${hops}]-(connected)
         RETURN nodes(path) AS nodes, relationships(path) AS rels
         LIMIT 500`,
        { personId },
      );
    });

    const nodesMap = new Map<string, GraphNode>();
    const relsMap = new Map<string, GraphRelationship>();

    for (const record of result.records) {
      for (const node of record.get("nodes")) {
        nodesMap.set(node.elementId, toGraphNode(node));
      }
      for (const rel of record.get("rels")) {
        relsMap.set(rel.elementId, toGraphRelationship(rel));
      }
    }

    return {
      nodes: Array.from(nodesMap.values()),
      relationships: Array.from(relsMap.values()),
    };
  });
};

8. Parameterized Queries

Always use parameters. Never concatenate user input into Cypher strings.

// ✅ Parameterized — safe, enables query plan caching
const result = await session.run(
  `MATCH (p:Person {id: $personId})-[r*1..2]-(connected)
   WHERE connected.timestamp >= $startDate
   RETURN p, r, connected
   LIMIT $limit`,
  { personId: id, startDate: startDate.toISOString(), limit: neo4j.int(100) },
);

// ❌ String interpolation — Cypher injection risk
const result = await session.run(
  `MATCH (p:Person {id: '${id}'})-[r*1..2]-(connected) RETURN p`,
);

9. Error Handling

import { Neo4jError } from "neo4j-driver";

export const handleNeo4jError = (error: unknown): never => {
  if (error instanceof Neo4jError) {
    switch (error.code) {
      case "Neo.ClientError.Security.Unauthorized":
        throw new Error("Invalid Neo4j credentials");
      case "Neo.ClientError.Security.Forbidden":
        throw new Error("User does not have permission for this operation");
      case "Neo.ClientError.Statement.SyntaxError":
        throw new Error(`Invalid Cypher query: ${error.message}`);
      case "Neo.ClientError.Schema.ConstraintValidationFailed":
        throw new Error(`Constraint violation: ${error.message}`);
      case "Neo.TransientError.Transaction.DeadlockDetected":
        throw new Error("Deadlock detected — retry the operation");
      default:
        throw new Error(`Neo4j error [${error.code}]: ${error.message}`);
    }
  }
  throw error;
};

Transient Error Retry

executeRead/executeWrite handle transient retries automatically. If using raw session.run(), catch and retry transient errors:

const isTransientError = (error: unknown): boolean =>
  error instanceof Neo4jError && error.code.startsWith("Neo.TransientError");

10. Connection Lifecycle

// App startup
const driver = initDriver(config.uri, config.user, config.password);
await driver.verifyConnectivity();

// App shutdown (or React cleanup)
useEffect(() => {
  return () => {
    closeDriver();
  };
}, []);

Anti-Patterns

Anti-Pattern Why It Fails Fix
Storing the driver in React state useState(neo4j.driver(...)) — driver is not serializable, gets recreated on re-render Store driver in a module-level variable
Creating a new driver per query Bypasses connection pooling, exhausts resources Create one driver at app startup, reuse it
Not closing sessions Connection pool exhaustion — queries start timing out Always close in finally or use withSession helper
String concatenation in Cypher Cypher injection vulnerability Always use parameterized queries
Ignoring neo4j.Integer type toNumber() silently overflows for values > Number.MAX_SAFE_INTEGER Use toString() for display, toBigInt() for arithmetic on large values
Using record.get('n').identity Internal numeric ID — can change across database restarts Use record.get('n').elementId (stable string identifier)
Not mapping Neo4j types before storing in Zustand Zustand stores (and JSON serialization) can't handle neo4j.Integer or neo4j.DateTime Always map with toNumber() / toString() before storing
Using session.run() for multi-statement transactions No automatic retry on transient errors, no transaction boundary Use session.executeRead(tx => ...) or session.executeWrite(tx => ...)
Weekly Installs
6
GitHub Stars
1
First Seen
Mar 3, 2026
Installed on
opencode6
gemini-cli4
github-copilot4
codex4
kimi-cli4
continue4