neo4j-driver-js
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 IMPERSONATEprivilege - 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 => ...) |