Build a Node.js Application with Cassandra

Connect Apache Cassandra to a Node.js application in about 10 minutes. You will build a simple user management app that creates a keyspace and table, inserts records using prepared statements, queries them, and cleans up.

What You’ll Build

A command-line Node.js script that:

  • Connects to a local Cassandra instance using the Apache Cassandra Node.js driver

  • Creates a keyspace and a users table

  • Inserts, queries, updates, and deletes rows using CQL and prepared statements

  • Shuts down cleanly

Prerequisites

  • Node.js 18 or later

  • npm

  • Docker (for running Cassandra locally)

Start Cassandra

Pull and start a single Cassandra node:

docker pull cassandra:latest
docker run --name cassandra -d -p 9042:9042 cassandra:latest

Wait 30-60 seconds for the node to initialize, then verify readiness:

docker exec cassandra cqlsh -e "DESCRIBE KEYSPACES"

Repeat the command until it returns a list of system keyspaces without an error.

The Node.js driver handles connection pooling and reconnection automatically, so you do not need to manage socket lifecycle in application code.

Initialize Your Project

mkdir cassandra-quickstart && cd cassandra-quickstart
npm init -y
npm install cassandra-driver

The cassandra-driver package is the Apache Cassandra Node.js Driver. It supports all major Cassandra features including prepared statements, token-aware routing, automatic paging, and speculative execution.

Connect to Cassandra

Create a file named app.js and add the following connection setup:

const cassandra = require('cassandra-driver');

const client = new cassandra.Client({
  contactPoints: ['127.0.0.1'],
  localDataCenter: 'datacenter1'
});

async function main() {
  await client.connect();
  console.log('Connected to Cassandra!');
}

main().catch(console.error);

Run it to confirm the connection works:

node app.js

You should see Connected to Cassandra! printed to the console.

contactPoints lists one or more seed addresses. localDataCenter must match the data center name reported by system.local. For a single-node Docker instance the default is datacenter1.

Create a Keyspace and Table

Replace the body of main() with the following. The connection call is still required first.

async function main() {
  await client.connect();
  console.log('Connected to Cassandra!');

  // Create keyspace
  await client.execute(`
    CREATE KEYSPACE IF NOT EXISTS quickstart
    WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}
  `);
  console.log('Keyspace ready.');

  // Create table
  await client.execute(`
    CREATE TABLE IF NOT EXISTS quickstart.users (
      id    uuid PRIMARY KEY,
      name  text,
      email text
    )
  `);
  console.log('Table ready.');
}

SimpleStrategy with replication_factor: 1 is appropriate for local development. Use NetworkTopologyStrategy in production clusters. The examples use random UUIDs because the row ID is only a lookup key for later updates and deletes.

Insert Data

Add an insert function below the table creation block. The driver auto-prepares statements when you pass { prepare: true }.

  // Insert rows
  const insertQuery = 'INSERT INTO quickstart.users (id, name, email) VALUES (?, ?, ?)';

  const alice = cassandra.types.Uuid.random();
  await client.execute(insertQuery, [alice, 'Alice', 'alice@example.com'], { prepare: true });

  const bob = cassandra.types.Uuid.random();
  await client.execute(insertQuery, [bob, 'Bob', 'bob@example.com'], { prepare: true });

  console.log('Inserted 2 rows.');
Pass { prepare: true } to auto-prepare statements for better performance. The driver caches prepared statement IDs per host, so subsequent executions skip the prepare round-trip.

cassandra.types.Uuid.random() generates a version 4 UUID. You can also use cassandra.types.TimeUuid.now() for time-ordered UUIDs.

Query Data

  // Read all rows
  const result = await client.execute('SELECT * FROM quickstart.users');
  console.log('Users:');
  for (const row of result.rows) {
    console.log(`  ${row.name} <${row.email}> (${row.id})`);
  }

For large tables use eachRow or pass { fetchSize: N } to control page size. The driver handles automatic paging when you iterate result.rows across pages.

Update and Delete

  // Update Alice's email
  await client.execute(
    'UPDATE quickstart.users SET email = ? WHERE id = ?',
    ['alice@newdomain.com', alice],
    { prepare: true }
  );
  console.log('Updated Alice.');

  // Delete Bob
  await client.execute(
    'DELETE FROM quickstart.users WHERE id = ?',
    [bob],
    { prepare: true }
  );
  console.log('Deleted Bob.');

Complete Application

Here is the full app.js script with a proper shutdown in the finally block:

const cassandra = require('cassandra-driver');

const client = new cassandra.Client({
  contactPoints: ['127.0.0.1'],
  localDataCenter: 'datacenter1'
});

async function main() {
  await client.connect();
  console.log('Connected to Cassandra!');

  // Keyspace
  await client.execute(`
    CREATE KEYSPACE IF NOT EXISTS quickstart
    WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}
  `);

  // Table
  await client.execute(`
    CREATE TABLE IF NOT EXISTS quickstart.users (
      id    uuid PRIMARY KEY,
      name  text,
      email text
    )
  `);

  const insertQuery = 'INSERT INTO quickstart.users (id, name, email) VALUES (?, ?, ?)';
  const alice = cassandra.types.Uuid.random();
  const bob   = cassandra.types.Uuid.random();

  await client.execute(insertQuery, [alice, 'Alice', 'alice@example.com'], { prepare: true });
  await client.execute(insertQuery, [bob,   'Bob',   'bob@example.com'],   { prepare: true });
  console.log('Inserted 2 rows.');

  // Read
  const result = await client.execute('SELECT * FROM quickstart.users');
  console.log('Users:');
  for (const row of result.rows) {
    console.log(`  ${row.name} <${row.email}> (${row.id})`);
  }

  // Update
  await client.execute(
    'UPDATE quickstart.users SET email = ? WHERE id = ?',
    ['alice@newdomain.com', alice],
    { prepare: true }
  );
  console.log('Updated Alice.');

  // Delete
  await client.execute(
    'DELETE FROM quickstart.users WHERE id = ?',
    [bob],
    { prepare: true }
  );
  console.log('Deleted Bob.');

  // Confirm final state
  const final = await client.execute('SELECT * FROM quickstart.users');
  console.log('Final users:');
  for (const row of final.rows) {
    console.log(`  ${row.name} <${row.email}>`);
  }
}

main()
  .catch(console.error)
  .finally(() => client.shutdown());

Run the complete script:

node app.js

Expected output:

Connected to Cassandra!
Inserted 2 rows.
Users:
  Alice <alice@example.com> (...)
  Bob <bob@example.com> (...)
Updated Alice.
Deleted Bob.
Final users:
  Alice <alice@newdomain.com>
This quickstart also works with Cassandra 6. When you are ready to go beyond basic CRUD, see ACID Transactions for BEGIN TRANSACTION and Constraints for schema validation.

What’s Next

  • Data Modeling — design tables around your query patterns before writing production schemas

  • ACID Transactions — use BEGIN TRANSACTION for lightweight multi-statement atomicity in Cassandra 6

  • Choose a Driver — compare all supported language drivers and their feature matrices

  • Vector Search — store and query vector embeddings alongside your application data