JavaScript Client Library for Geode
The Geode JavaScript client enables Node.js applications to connect to Geode using QUIC + TLS 1.3 transport. It provides async/await patterns, connection pooling, and GQL conformance profile support for building high-performance graph-powered applications.
Introduction
The JavaScript client is designed for modern Node.js development:
- Async/Await Native: First-class Promise support throughout
- Connection Pooling: Efficient connection management for high-throughput scenarios
- Stream Support: Handle large result sets with streaming APIs
- Framework Agnostic: Works with Express, Fastify, Koa, NestJS, and more
- ESM and CommonJS: Supports both module systems
Requirements
- Node.js 18.0 or later (for native QUIC support)
- npm 8.0+ or yarn 1.22+
Installation
# Using npm
npm install @geodedb/client
# Using yarn
yarn add @geodedb/client
# Using pnpm
pnpm add @geodedb/client
Quick Start
Basic Connection
const { Client } = require('@geodedb/client');
async function main() {
// Create client
const client = new Client({
host: 'localhost',
port: 3141,
});
// Connect
const conn = await client.connect();
try {
// Execute query
const result = await conn.query(
'MATCH (p:Person) RETURN p.name AS name, p.age AS age'
);
// Process results
for (const row of result.rows) {
console.log(`${row.name}: ${row.age} years old`);
}
} finally {
// Always close connection
await conn.close();
}
}
main().catch(console.error);
ES Modules
import { Client } from '@geodedb/client';
const client = new Client({ host: 'localhost', port: 3141 });
const conn = await client.connect();
const { rows } = await conn.query('RETURN "Hello, Geode!" AS greeting');
console.log(rows[0].greeting);
await conn.close();
Connection Configuration
Full Configuration Options
const client = new Client({
// Connection settings
host: 'geode.example.com',
port: 3141,
// TLS configuration
tls: {
ca: fs.readFileSync('/path/to/ca.crt'),
cert: fs.readFileSync('/path/to/client.crt'),
key: fs.readFileSync('/path/to/client.key'),
rejectUnauthorized: true, // Verify server certificate
},
// Client identification
clientName: 'my-application',
clientVersion: '1.0.0',
// Query settings
defaultPageSize: 1000,
queryTimeout: 30000, // 30 seconds
// Connection settings
connectTimeout: 10000, // 10 seconds
idleTimeout: 60000, // 1 minute
});
Environment Variables
// Load configuration from environment
const client = new Client({
host: process.env.GEODE_HOST || 'localhost',
port: parseInt(process.env.GEODE_PORT) || 3141,
tls: process.env.GEODE_TLS_ENABLED === 'true' ? {
ca: fs.readFileSync(process.env.GEODE_CA_PATH),
} : undefined,
});
Executing Queries
Parameterized Queries
Always use parameters to prevent injection and improve performance:
// Good: Parameterized query
const result = await conn.query(
'MATCH (u:User {email: $email}) RETURN u',
{ email: userInput }
);
// Bad: String interpolation (vulnerable to injection)
// const result = await conn.query(`MATCH (u:User {email: "${userInput}"}) RETURN u`);
Parameter Types
// String parameters
await conn.query('CREATE (p:Person {name: $name})', { name: 'Alice' });
// Number parameters
await conn.query('MATCH (p:Person) WHERE p.age > $age RETURN p', { age: 21 });
// Boolean parameters
await conn.query('MATCH (p:Product) WHERE p.active = $active RETURN p', { active: true });
// Null parameters
await conn.query('MATCH (p:Person) SET p.middleName = $middle', { middle: null });
// Array parameters
await conn.query('MATCH (p:Person) WHERE p.id IN $ids RETURN p', {
ids: ['id1', 'id2', 'id3']
});
// Object/Map parameters
await conn.query('CREATE (c:Config {settings: $settings})', {
settings: { theme: 'dark', notifications: true }
});
// Date parameters
await conn.query('CREATE (e:Event {date: $date})', {
date: new Date('2026-01-28T10:00:00Z')
});
Execute vs Query
// Use query() for SELECT operations that return data
const { rows, schema } = await conn.query(
'MATCH (p:Person) RETURN p.name, p.age'
);
// Use execute() for mutations that don't need results
const { summary } = await conn.execute(
'CREATE (p:Person {name: $name, age: $age})',
{ name: 'Bob', age: 30 }
);
console.log(`Created ${summary.nodesCreated} nodes`);
Connection Pooling
Creating a Pool
const { Pool } = require('@geodedb/client');
const pool = new Pool({
host: 'localhost',
port: 3141,
// Pool configuration
minConnections: 5, // Minimum idle connections
maxConnections: 20, // Maximum total connections
acquireTimeout: 5000, // Wait time for connection
idleTimeout: 60000, // Close idle connections after
maxLifetime: 3600000, // Maximum connection age (1 hour)
});
// Initialize pool
await pool.initialize();
Using the Pool
// Method 1: Manual acquire/release
async function queryWithPool() {
const conn = await pool.acquire();
try {
const result = await conn.query('MATCH (n) RETURN count(n) AS total');
return result.rows[0].total;
} finally {
pool.release(conn);
}
}
// Method 2: Using withConnection helper
async function queryWithHelper() {
return pool.withConnection(async (conn) => {
const result = await conn.query('MATCH (n) RETURN count(n) AS total');
return result.rows[0].total;
});
}
// Method 3: Using connection callback
const total = await pool.query('MATCH (n) RETURN count(n) AS total');
Pool Events
pool.on('connect', (conn) => {
console.log('New connection established');
});
pool.on('acquire', (conn) => {
console.log('Connection acquired from pool');
});
pool.on('release', (conn) => {
console.log('Connection released to pool');
});
pool.on('error', (err) => {
console.error('Pool error:', err);
});
pool.on('close', () => {
console.log('Pool closed');
});
Graceful Shutdown
// Handle shutdown signals
process.on('SIGTERM', async () => {
console.log('Shutting down...');
await pool.drain(); // Wait for active connections
await pool.close(); // Close all connections
process.exit(0);
});
Transactions
Basic Transaction
async function transferFunds(fromId, toId, amount) {
const conn = await pool.acquire();
try {
await conn.beginTransaction();
try {
// Debit source account
await conn.execute(
'MATCH (a:Account {id: $id}) SET a.balance = a.balance - $amount',
{ id: fromId, amount }
);
// Credit destination account
await conn.execute(
'MATCH (a:Account {id: $id}) SET a.balance = a.balance + $amount',
{ id: toId, amount }
);
await conn.commit();
return { success: true };
} catch (err) {
await conn.rollback();
throw err;
}
} finally {
pool.release(conn);
}
}
Transaction with Savepoints
async function complexOperation(conn) {
await conn.beginTransaction();
try {
// Create user
await conn.execute(
'CREATE (u:User {id: $id, name: $name})',
{ id: 'user_001', name: 'Alice' }
);
// Create savepoint before risky operation
await conn.savepoint('before_preferences');
try {
// This might fail
await conn.execute(
'MATCH (u:User {id: $id}) CREATE (u)-[:PREFERS]->(:Category {name: $cat})',
{ id: 'user_001', cat: 'InvalidCategory' }
);
} catch (err) {
// Rollback to savepoint, not entire transaction
await conn.rollbackToSavepoint('before_preferences');
// Use default instead
await conn.execute(
'MATCH (u:User {id: $id}) CREATE (u)-[:PREFERS]->(:Category {name: "General"})',
{ id: 'user_001' }
);
}
await conn.commit();
} catch (err) {
await conn.rollback();
throw err;
}
}
Streaming Large Results
Using Async Iterators
async function streamLargeDataset() {
const conn = await client.connect();
try {
const stream = conn.stream(
'MATCH (p:Product) RETURN p.id, p.name, p.price',
{},
{ batchSize: 1000 }
);
let count = 0;
for await (const row of stream) {
// Process row
await processProduct(row);
count++;
if (count % 10000 === 0) {
console.log(`Processed ${count} products`);
}
}
console.log(`Total: ${count} products`);
} finally {
await conn.close();
}
}
Streaming to File
const fs = require('fs');
const { pipeline } = require('stream/promises');
const { Transform } = require('stream');
async function exportToCSV(filename) {
const conn = await client.connect();
try {
const queryStream = conn.stream(
'MATCH (u:User) RETURN u.id, u.name, u.email'
);
const csvTransform = new Transform({
objectMode: true,
transform(row, encoding, callback) {
const line = `${row.id},${row.name},${row.email}\n`;
callback(null, line);
}
});
const fileStream = fs.createWriteStream(filename);
// Write header
fileStream.write('id,name,email\n');
// Stream data
await pipeline(queryStream, csvTransform, fileStream);
} finally {
await conn.close();
}
}
Framework Integration
Express.js
const express = require('express');
const { Pool } = require('@geodedb/client');
const app = express();
const pool = new Pool({ host: 'localhost', port: 3141 });
// Middleware to attach connection
app.use(async (req, res, next) => {
req.geode = await pool.acquire();
res.on('finish', () => pool.release(req.geode));
next();
});
// Route handlers
app.get('/users/:id', async (req, res) => {
try {
const { rows } = await req.geode.query(
'MATCH (u:User {id: $id}) RETURN u',
{ id: req.params.id }
);
if (rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.json(rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.get('/users/:id/friends', async (req, res) => {
const { rows } = await req.geode.query(
`MATCH (u:User {id: $id})-[:FRIENDS_WITH]->(f:User)
RETURN f.id AS id, f.name AS name`,
{ id: req.params.id }
);
res.json(rows);
});
app.listen(3000);
Fastify
const fastify = require('fastify')();
const { Pool } = require('@geodedb/client');
// Register Geode plugin
fastify.register(async (fastify) => {
const pool = new Pool({ host: 'localhost', port: 3141 });
await pool.initialize();
fastify.decorate('geode', pool);
fastify.addHook('onClose', async () => {
await pool.close();
});
});
// Routes
fastify.get('/products', async (request, reply) => {
const { category, minPrice, maxPrice } = request.query;
const { rows } = await fastify.geode.query(
`MATCH (p:Product)
WHERE ($category IS NULL OR p.category = $category)
AND ($minPrice IS NULL OR p.price >= $minPrice)
AND ($maxPrice IS NULL OR p.price <= $maxPrice)
RETURN p.id, p.name, p.price, p.category
ORDER BY p.price`,
{ category, minPrice, maxPrice }
);
return rows;
});
fastify.listen({ port: 3000 });
NestJS
// geode.module.ts
import { Module, Global } from '@nestjs/common';
import { Pool } from '@geodedb/client';
@Global()
@Module({
providers: [
{
provide: 'GEODE_POOL',
useFactory: async () => {
const pool = new Pool({
host: process.env.GEODE_HOST,
port: parseInt(process.env.GEODE_PORT),
});
await pool.initialize();
return pool;
},
},
],
exports: ['GEODE_POOL'],
})
export class GeodeModule {}
// users.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { Pool } from '@geodedb/client';
@Injectable()
export class UsersService {
constructor(@Inject('GEODE_POOL') private readonly pool: Pool) {}
async findById(id: string) {
const { rows } = await this.pool.query(
'MATCH (u:User {id: $id}) RETURN u',
{ id }
);
return rows[0];
}
async findFriends(userId: string) {
const { rows } = await this.pool.query(
`MATCH (u:User {id: $id})-[:FRIENDS_WITH]->(f:User)
RETURN f`,
{ id: userId }
);
return rows;
}
}
Error Handling
Error Types
const {
GeodeError,
ConnectionError,
QueryError,
TransactionError,
TimeoutError,
} = require('@geodedb/client');
async function safeQuery(conn, query, params) {
try {
return await conn.query(query, params);
} catch (err) {
if (err instanceof ConnectionError) {
console.error('Connection failed:', err.message);
// Retry with new connection
} else if (err instanceof QueryError) {
console.error('Query failed:', err.code, err.message);
// Log query details for debugging
} else if (err instanceof TimeoutError) {
console.error('Query timed out');
// Consider increasing timeout or optimizing query
} else {
throw err;
}
}
}
GQL Error Codes
const { GqlErrorCode } = require('@geodedb/client');
try {
await conn.query('INVALID SYNTAX');
} catch (err) {
switch (err.code) {
case GqlErrorCode.SYNTAX_ERROR:
console.error('Invalid GQL syntax');
break;
case GqlErrorCode.CONSTRAINT_VIOLATION:
console.error('Constraint violated');
break;
case GqlErrorCode.NOT_FOUND:
console.error('Entity not found');
break;
default:
console.error('Unknown error:', err.code);
}
}
Performance Best Practices
Connection Reuse
// Bad: New connection per request
app.get('/data', async (req, res) => {
const conn = await client.connect(); // Expensive!
const result = await conn.query('...');
await conn.close();
res.json(result);
});
// Good: Use connection pool
app.get('/data', async (req, res) => {
const result = await pool.query('...'); // Reuses connection
res.json(result);
});
Batch Operations
// Process items in batches
async function batchInsert(items, batchSize = 100) {
const conn = await pool.acquire();
try {
await conn.beginTransaction();
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
await conn.execute(
`UNWIND $items AS item
CREATE (p:Product {
id: item.id,
name: item.name,
price: item.price
})`,
{ items: batch }
);
}
await conn.commit();
} catch (err) {
await conn.rollback();
throw err;
} finally {
pool.release(conn);
}
}
Related Topics
- TypeScript : TypeScript type definitions and usage
- Node.js : Node.js specific patterns
- Client Libraries : Overview of all client libraries
- Protocol : Wire protocol specification
- Performance : Query optimization tips
Further Reading
- Node.js Client API Reference: Complete API documentation
- Express Integration Guide: Building REST APIs with Geode
- GraphQL Integration: Using Geode as a GraphQL data source
- Serverless Deployment: Using Geode with AWS Lambda, Vercel
- Testing Guide: Unit and integration testing patterns
Browse the tagged content below for JavaScript and Node.js documentation, tutorials, and examples.