Skip to content
Colin Wren
Twitter

Adding an API to your Neo4J graph with @neo4j/graphql

JavaScript, Software Development, Technology5 min read

A guide on adding an API to your Neo4J graph with the @neo4j/graphql library.

In order to prove that the concept behind my new product — Clime (an app for connecting product knowledge across multiple tools) was technically feasible, I created a proof-of-concept using Neo4J as the problem lends itself well to graph databases.

After creating a basic set of nodes and establishing relationships between them I was able to execute queries that allowed one node to find linked nodes that weren’t directly related using a node that both sets of nodes were related to.

Graph in Neo4J showing the test data I used
The example data I used
Graph in Neo4J showing the result of a query I used to validate the idea behind Clime
Query showing the Tests linked to UserStorys that a single Mockup is linked to

This validated the technical aspects of my idea so I went ahead with the next step of validating my idea which was to build a landing page to drum up interest in the product.

Once the landing page was live I decided to revisit the proof-of-concept as something was nagging at me — The graph database provided the means to query the product knowledge graph but I hadn’t yet planned out how I’d build an application on top of it.

The vision I have for Clime is to be a centralised repository for connecting product knowledge that is accessible from within the tools that the team uses (e.g. you could right-click a design in Figma and get a list of user stories it’s linked to) via series of plugins and integrations built by myself or a community of users.

While it would likely be easy enough to build a library to deal with querying Neo4J directly I felt that a REST or GraphQL API would be easier for others to work with, which is key if I’m looking to build a product that people can build their own programs for.

After a bit of research, I was really excited to see that Neo4J already has a solution for building a GraphQL wrapper around a graph database — the @neo4j/graphql package.

This package when combined with the Neo4J driver (a library for accessing a Neo4J instance) allows you to create a GraphQL schema for the nodes in your Neo4J graph and provides some handy GraphQL directives to make querying relationships easy to define. The GraphQL schema can then be used with Apollo or your GraphQL server of choice.

1import { gql, ApolloServer } from 'apollo-server'
2import { Neo4jGraphQL } from '@neo4j/graphql'
3import neo4j from 'neo4j-driver'
4
5const typeDef = gql``; // Type Defs for schema
6
7const driver = neo4j.driver(
8 'http://localhost:7687', // Bolt URL for Neo4J
9 neo4j.auth.basic('neo4j', 's3cr3t') // Username & password for Neo4J
10);
11
12const neoSchema = new Neo4jGraphQL({ typeDefs, driver });
13
14neoSchema.getSchema().then((schema) => {
15 const server = new ApolloServer({
16 schema
17 });
18
19 server.listen().then((serverConfig) => {
20 console.log('GraphQL server started', serverConfig)
21 })
22})
Setting up the Neo4J GraphQL server isn’t that different from setting up a standard Apollo server

Creating a GraphQL schema for Neo4J

Before we can build an API we need to have a data model we’re looking to expose via that API. For an API over a graph database, this model needs to include both the nodes and edges as the relationships the edges represent will be key for returning linked data.

To keep things simple I’m going to re-use the model I used for the Clime proof-of-concept

  • Mockup — (A design, :VISUALISES a UserStory )
  • UserStory — (A definition of the system’s behaviour, :DEFINES a Product )
  • Product — (A Product, top-level object)
  • Test — (A test executed to ensure the system behaves correctly, :TESTS a UserStory )

Based on this model here’s a Cypher query that will add a sample set of data to Neo4J:

1// Create Product
2CREATE (ChocolateBox:Product {id: 'PRODUCT-1', name:'Chocolate Box'})
3
4// Create Figma mockups
5CREATE (LandingPage:Mockup {id: 'MOCKUP-1', name:'Landing Page', nodeId:'424:1224'})
6CREATE (ChocolatePLP:Mockup {id: 'MOCKUP-2', name:'Chocolate PLP', nodeId:'424:1233'})
7CREATE (FlowerPLP:Mockup {id: 'MOCKUP-4', name:'Flower PLP', nodeId:'424:1242'})
8
9// Create User Stories
10CREATE (ViewFlowersList:UserStory {id:'USER-STORY-1', name:'Customer Views Flowers Product List'})
11CREATE (ViewChocolatesList:UserStory {id:'USER-STORY-2', name:'Customer Views Chocolates Product List'})
12
13// Create Test Cases
14CREATE (TD1:Test {id:'TEST-1', name:'User Adds Flowers to Basket'})
15CREATE (TD2:Test {id:'TEST-2', name:'User Adds Chocolates to Basket'})
16CREATE (TD3:Test {id:'TEST-3', name:'New User buys Chocolates'})
17CREATE (TD4:Test {id:'TEST-4', name:'New User buys Flowers'})
18CREATE (TD5:Test {id:'TEST-5', name:'Existing User buys Chocolates'})
19CREATE (TD6:Test {id:'TEST-6', name:'Existing User buys Flowers'})
20
21// Link User Stories to Product
22CREATE (ViewFlowersList)-[:DEFINES {author: 'Colin'}]->(ChocolateBox)
23CREATE (ViewChocolatesList)-[:DEFINES {author: 'Not Colin'}]->(ChocolateBox)
24
25// Link Mockups to Story
26CREATE (LandingPage)-[:VISUALISES]->(ViewFlowersList)
27CREATE (LandingPage)-[:VISUALISES]->(ViewChocolatesList)
28CREATE (ChocolatePLP)-[:VISUALISES]->(ViewChocolatesList)
29CREATE (FlowerPLP)-[:VISUALISES]->(ViewFlowersList)
30
31// Link Tests to user story
32CREATE (TD1)-[:TESTS]->(ViewFlowersList)
33CREATE (TD2)-[:TESTS]->(ViewChocolatesList)
34CREATE (TD3)-[:TESTS]->(ViewChocolatesList)
35CREATE (TD4)-[:TESTS]->(ViewChocolatesList)
36CREATE (TD5)-[:TESTS]->(ViewChocolatesList)
37CREATE (TD6)-[:TESTS]->(ViewChocolatesList)
Test Data used to create the Graph shown above

In order to create the GraphQL API for this model we then need to create type definitions so the Neo4jGraphQL constructor knows how to map the nodes in Neo4J to GraphQL objects. To start with we can just include the fields these objects will expose (we’ll cover linked nodes later).

1type Product {
2 id: ID! @id
3 name: String
4 }
5 type Mockup {
6 id: ID! @id
7 name: String
8 nodeId: String
9 }
10 type UserStory {
11 id: ID! @id
12 name:String
13 }
14 type Test {
15 id: ID! @id
16 name: String
17 }
A basic GraphQL schema containing fields for the properties on the nodes. The @id directive means that the ID field is a unique reference for when we need to connect nodes (more on that later)

Once the schema is set in Apollo you can then query those types and fields as you would normally do with GraphQL.

1query Query {
2 products {
3 id
4 name
5 }
6 mockups {
7 id
8 name
9 }
10 userStories {
11 id
12 name
13 }
14 tests {
15 id
16 name
17 }
18}
19
20"""
21Returns
22{
23 data: {
24 products: [
25 {
26 id: 'foo',
27 name: 'bar'
28 }
29 ],
30 mockups: [
31 {
32 id: 'foo',
33 name: 'bar'
34 }
35 ],
36 userStories: [
37 {
38 id: 'foo',
39 name: 'bar'
40 }
41 ],
42 tests: [
43 {
44 id: 'foo',
45 name: 'bar'
46 }
47 ]
48 }
49}
50"""
Query for the basic schema objects

Returning linked nodes

The basic fields that are currently being returned are enough to see the values of the nodes but the main reason for using a graph database is to be able to find related nodes and the @neo4j/graphql library offers a few GraphQL directives to make this easy to set up.

The @relationship directive allows you to add fields for nodes that are directly linked to the source node being defined. It takes two arguments; type which is the type of edge that links the two nodes and direction which is the direction of the relationship ( IN for the other node defining the edge, OUT if the source node defines the edge).

Returning data from the edges that link nodes

A @relationship field can be expanded using the @relationshipProperties directive to include properties set during the definition of the edge between two nodes. The @relationship field definition is a properties argument which matches a type that implements the @relationshipProperties interface.

When querying the connected field you won’t be able to access the edge data from within the field, instead, you’ll need to query the [FIELD NAME]Connection (e.g. userStoriesConnection) object which contains the edge data and the node

Returning data from non-directly linked nodes

If there’s no clear relationship between the source node and another node that you want to return because they share a common, related node you can use the @cypher directive to execute a Cypher query and return the results as a field.

Here’s how you’d use these directives in your schema:

1type Product {
2 id: ID! @id
3 name: String
4 userStories: [UserStory!]! @relationship(type: "DEFINES", properties: 'Defines', direction: IN)
5 }
6 type Mockup {
7 id: ID! @id
8 name: String
9 nodeId: String
10 userStories: [UserStory!]! @relationship(type: "VISUALISES", direction: OUT)
11 tests: [Test] @cypher(
12 statement: """
13 MATCH (this)-[:VISUALISES]->(story)<-[:TESTS]-(test)
14 RETURN distinct test
15 """
16 )
17 }
18 type UserStory {
19 id: ID! @id
20 name:String
21 mockups: [Mockup!]! @relationship(type: "VISUALISES", direction: IN)
22 tests: [Test!]! @relationship(type: "TESTS", direction: IN)
23 }
24 type Test {
25 id: ID! @id
26 name: String
27 lastResult: String
28 userStories: [UserStory!]! @relationship(type: "TESTS", direction: OUT)
29 }
30 interface Defines @relationshipProperies {
31 author: String
32 }
Basic schema updated with fields for getting directly linked and indirectly linked nodes as well as properties from the edges linking UserStory to Product

And an example query for fetching the relationships:

1query Query {
2 products {
3 name
4 userStoriesConnection {
5 edges {
6 author
7 node {
8 name
9 }
10 }
11 }
12 }
13 mockups {
14 name
15 tests {
16 name
17 }
18 userStories {
19 name
20 }
21 }
22}
23
24"""
25Returns
26{
27 data {
28 products [
29 {
30 name: 'Chocolate Box',
31 userStoriesConnection: {
32 edges: [
33 {
34 author: 'Colin',
35 node: {
36 name: 'Customer Views Chocolates Product List'
37 }
38 }
39 ]
40 }
41 }
42 ],
43 mockups [
44 {
45 name: 'Landing Page',
46 tests: [
47 {
48 name: 'Existing User buys Flowers'
49 }
50 ]
51 }
52 ]
53 }
54}
55"""
In order to access the edge fields you need to query the connection, not the linked node but you can access the linked node via the connection

Writing data to Neo4J via GraphQL

The @neo4j/graphql library creates a useful set of mutations for performing Create-Read-Update-Delete (CRUD) actions on the types defined in the GraphQL schema we’ve already covered (read above), but here’s how you can create, update and delete nodes and edges.

Adding nodes and edges

To create a new node you can call the create[TYPE NAME] (e.g createTest) mutation which will take an input object which is the non-ID fields for the type. An example of this would be:

1"""
2Input looks like
3{
4 "input": {
5 "name": "A new test"
6 }
7}
8"""
9mutation Mutation($input: [TestCreateInput!]!) {
10 createTests(input: $input) {
11 tests {
12 id
13 }
14 }
15}
16"""
17Response looks like
18{
19 data: {
20 createTests: {
21 tests: [
22 {
23 id: 'ID of the newly created test'
24 }
25 ]
26 }
27 }
28}
29"""
The input object is essentially the same as the type but without the ID field as this is set automatically. You can return the ID after the new node is created

Updating nodes and edges

To update a node, you can call the update[TYPE NAME] (e.g. updateTest) mutation which will take a where object (for finding the node to update) and an update object with the fields to be updated on that node.

1"""
2Arguments look like
3{
4 "where": {
5 "id": "ID of the node to update"
6 },
7 "update": {
8 "name": "Updated test"
9 }
10}
11"""
12mutation Mutation($where: TestWhere, $update: TestUpdateInput) {
13 updateTests(where: $where, update: $update) {
14 tests {
15 name
16 }
17 }
18}
19"""
20Response looks like
21{
22 data: {
23 updateTests: [
24 {
25 name: 'Updated test'
26 }
27 ]
28 }
29}
30"""
In order to update a node you need to define a where the object which will help Neo4J find the node to update, the update object defines the fields to change on the node

You can use the same update[TYPE NAME] mutation to create new edges between the nodes by defining a connect or connectOrCreate object with a where object to find the source node and a where object to find the target node (the difference is that connectOrCreate will create the node being linked to if it doesn’t exist).

1"""
2Arguments look like
3{
4 "where": {
5 "id": "ID of the node to update"
6 },
7 "update": {
8 "userStories": [
9 {
10 "connect": [
11 {
12 "where": {
13 "id": "ID of the node to connect"
14 }
15 }
16 ]
17 }
18 ]
19 }
20}
21"""
22mutation Mutation($where: TestWhere, $update: TestUpdateInput) {
23 updateTests(where: $where, update: $update) {
24 tests {
25 name
26 userStories {
27 name
28 }
29 }
30 }
31}
32"""
33Response looks like
34{
35 data: {
36 updateTests: [
37 {
38 name: 'Updated test',
39 userStories: [
40 {
41 name: 'Linked User Story'
42 }
43 ]
44 }
45 ]
46 }
47}
48"""
Similar to the update mutation but instead of ‘update’-ing a node we ‘connect’ a node based on its field that has the relationship directive on it.

Removing nodes and edges

If you want to remove an edge between two nodes you can use the same update[TYPE NAME] mutation but instead of a connect object you define a disconnect object with a where object to find the node to remove the link with.

1"""
2Arguments look like
3{
4 "where": {
5 "id": "ID of the node to update"
6 },
7 "update": {
8 "userStories": [
9 {
10 "disconnect": [
11 {
12 "where": {
13 "id": "ID of the node to connect"
14 }
15 }
16 ]
17 }
18 ]
19 }
20}
21"""
22mutation Mutation($where: TestWhere, $update: TestUpdateInput) {
23 updateTests(where: $where, update: $update) {
24 tests {
25 name
26 userStories {
27 name
28 }
29 }
30 }
31}
32"""
33Response looks like
34{
35 data: {
36 updateTests: [
37 {
38 name: 'Updated test',
39 userStories: []
40 }
41 ]
42 }
43}
44"""
The disconnect operation is basically the same as the connect operation but with the disconnect name

Removing a node is done using the delete[TYPE NAME] (e.g. deleteTest) mutation with a where object to find the node to delete.

1"""
2Input looks like
3{
4 "where": {
5 "id": "ID of the node to delete"
6 }
7}
8"""
9mutation Mutation($where: TestWhere) {
10 deleteTests(where: $where) {
11 nodesDeleted
12 relationshipsDeleted
13 }
14}
15"""
16Response looks like
17{
18 "data": {
19 "deleteTests": {
20 "nodesDeleted": 1,
21 "relationshipsDeleted": 0
22 }
23 }
24}
25"""
Deleting a node, similar to updating nodes requires a where object. Because the node will be deleted the only fields that can be returned are the number of nodes deleted and the relationships that were deleted

Using the API within an app

There are a number of GraphQL clients that you can use within your app to pull data from the Neo4J graph using GraphQL.

The proof-of-concept repo linked at the top of the page contains a basic Figma plugin I created to use the GraphQL API, it uses the Apollo client for React which exposes the Apollo client via a React Context and has useQuery and useMutation hooks to perform operations within lower-level components.

These hooks are performing the same queries and mutations as listed above so it’s really easy to build up these calls by using the Apollo Sandbox and copying them over.

Summary

The @neo4j/graphql library creates a really powerful API on top of Neo4J that makes it really easy for those with little graph database experience (like myself) to build apps that can leverage the benefits a graph database offers when working with relationships between records.

The CRUD examples and directives I’ve covered are really only at the surface of functionality the library has so I highly recommend reading the Neo4J GraphQL library documentation as it’s very well organised and insightful.

You can find my proof-of-concept code which contains the Neo4J test data, GraphQL server, and example Figma plugin I created to prove my idea on my Gitlab:

Colin Wren / Neo4J and GraphQL example