Using the Neo4j Driver with NodeJS

After writing all about how to use the official Neo4j Drivers in a Spring application, I thought it would make sense to also detail how to use Neo4j with my language of choice, NodeJS.  Over the past few years I’ve written many applications in NodeJS that talk to Neo4j applications, to the point where I built an OGM to take care of the frustrating CRUD boilerplate when setting up a Neo4j project in node.  More about that later…

Dependencies

To get started, you’ll need to download and install nodejs and npm (or yarn if you prefer).  Once you’ve initiated your project, you can install the driver via npm.

npm install --save neo4j-driver

Creating a Driver instance

Now the driver can be included in the project.  The official drivers for all languages are namespaced by version, so we need to keep that in mind when creating a driver.

CommonJS
const neo4j = require('neo4j').v1;
const driver = new neo4j.driver("bolt://localhost:7687", neo4j.auth.driver("neo4j", "p455word"));

Or using ES6 import:

ES6
import { v1 as neo4j } from 'neo4j-driver';
const driver = new neo4j.driver("bolt://localhost:7687", neo4j.auth.driver("neo4j", "p455word"));

In both of these examples, I’m creating a new instance of the driver and connecting to bolt://localhost:7687. I’ve used a basic auth token, but as with Java Neo4j also supports Kerberos tokens and custom authentication schemes.

Providing the bolt+routing scheme to the connection string will ensure a Routing Driver is instantiated.  This will balance read and write queries across a Causal Cluster.  Where you are using a multi-datacenter setup or advanced routing policies, you can append a routing context to the end of the connection string.

bolt+routing://localhost:7687?policy=uk

Running a Query

To run one or more queries, you’ll first need to create a Session.  These sessions act as a container for a logical sequence of transactions (queries) and will borrow connections from the driver’s connection pool as necessary.  These sessions should be considered lightweight and disposable.

const session = driver.session();

Inside a session you can run three types of transactions.  Running queries inside atomic transactions allow you to you to complete an entire unit of work and committing once all statements have succeeded.  Should anything go wrong, the database can be rolled back to it’s original state.

Read or Write?

When creating a session, you can declare a mode as either read or write.  This can ensure that the driver routes the queries to the right place.  Constants for read and write sessions can be taken from v1.session.READ or v1.session.WRITE.

const session = driver.session(neo4j.session.READ);

Auto-commit Transactions

Auto-commit transactions are sent to the server and acknowledged immediately.  We can do by calling the run method on the session.  This method accepts two parameters, a parameterised Cypher query string and an optional object containing parameters in key, value format.

const cypher = "MATCH (p:Person {name: {name} }) RETURN count(p) AS count";
const params = { name: "Adam" };
session.run(cypher, params);
 

It is recommended that these transactions are only used for one-off statements and not used in production. 

Read & Write Transactions

We can run a transaction in one of two ways, either by calling session.beginTransaction() which returns a transaction object, or calling session.readTransaction() or session.writeTransaction() with a function containing the work for that transaction.  The first argument for this function will be a transaction.  We can use this to run queries.

driver.writeTransaction(tx => {
tx.run("CREATE (p:Person { name: {name} });", { name: "Adam" });
});

Declarative Syntax

The more declarative approach uses session.beginTransaction().  This returns a transaction which can be used similarly to above

const tx = session.beginTransaction();

tx.run("CREATE (p:Person { name: {name} });", { name: "Adam" })
.then(res => {
// Run another query with the tx variable...
})
.then(() => {
// Everything is OK, the transaction will be committed
})
.catch(e => {
// The transaction will be rolled back, now handle the error.
});

Transactions will be automatically committed once the function succeeds, or rolled back if an error is thrown during the chain.

Consuming Results

There are two ways of a consuming results.

Using Promises

The previous example uses the method.  When the entire query is completed, the results will be collected and can be acted on at once.  This method will return an instance of a  Statement Result.  This contains information about the query and an array of records which can be accessed through the records property. The values returned in a Record can be accessed using the get method.

session.run("MATCH (p:Person) RETURN p.name AS name")
.then(result => {
console.log(result.records.length); // Number of records returned

return result.records.map(record => { // Iterate through records
console.log( record.get("name") ); // Access the name property from the RETURN statement
});
})
.then(() => session.close()); // Always remember to close the session

Subscribing to Streams

You may have noticed that when a query is executed in the browser, you see two numbers; when the database starts to stream results and when the query is executed.  If you would like to consume the results as they are streamed, you can use the subscribe method.   The subscribe function accepts an object with callbacks for when a record is received (onNext), when the stream is completed (onCompleted) and on error (onError).

session.run("MATCH (p:Person) RETURN p.name AS name")
.subscribe({
onNext: (record) => {
console.log(record.get('name')); // Consume the same Record object as above
},
onCompleted: function () {
session.close();
},
onError: function (error) {
console.log(error);
}
});

Gotcha: Integers

While the Neo4j type system supports 64-bit integers, JavaScript is unable to handle these correctly.  In order to support Cypher’s type system, the driver will convert integer values into an instance of a neo4j Integer.  In order to work with these

Read more on Integers.

What about CRUD operations?

Generic CRUD operations can be a pain to implement.  Writing the boilerplate to run write operations can take time, and for this reason I wrote Neode.  The aim of Neode is to make working with Neo4j as easy as possible – it uses dotenv to take variables from a .env file and supports schema generation, validation and UUID’s out of the box.
For more information, head to github.com/adam-cowley/neode or check out the example project based on the Movies sandbox.

If you don’t like the look of that, there is also OGMNeo which takes a slightly different approach but also contains a comprehensive API.

TL;DR – Code Example

// Require Neo4j
const neo4j = require('neo4j-driver').v1;

// Create Driver
const driver = new neo4j.driver("bolt://localhost:7687", neo4j.auth.basic("neo4j", "neo"));

// Create Driver session
const session = req.driver.session();

// Run Cypher query
const cypher = 'MATCH (n) RETURN count(n) as count';

session.run(cypher)
.then(result => {
// On result, get count from first record
const count = result.records[0].get('count');

// Log response
console.log( count.toNumber() );
})
.catch(e => {
// Output the error
console.log(e);
})
.then(() => {
// Close the Session
return session.close();
})
.then(() => {
// Close the Driver
return driver.close();
});

Further Reading

The Neo4j Developer Page provides a comprehensive guide to all options available to you and example projects. This gist provides a basic example of how to use Neo4j in an Express middleware.

These drivers can also be used in front end developments, albeit with the caveat that you’ll need to connect directly to the Bolt server.  This is the way that the Neo4j Browser works.

If you’re a fan of VueJS, I have also created vue-neo4j to provide a simple interface for executing Cypher queries Vue components.

Happy coding!