Adding an API to your Neo4J graph with @neo4j/graphql
— JavaScript, Software Development, Technology — 5 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.


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 schema6
7const driver = neo4j.driver(8 'http://localhost:7687', // Bolt URL for Neo4J9 neo4j.auth.basic('neo4j', 's3cr3t') // Username & password for Neo4J10);11
12const neoSchema = new Neo4jGraphQL({ typeDefs, driver });13
14neoSchema.getSchema().then((schema) => {15 const server = new ApolloServer({16 schema17 });18
19 server.listen().then((serverConfig) => {20 console.log('GraphQL server started', serverConfig)21 })22})
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
aUserStory
)UserStory
— (A definition of the system’s behaviour,:DEFINES
aProduct
)Product
— (A Product, top-level object)Test
— (A test executed to ensure the system behaves correctly,:TESTS
aUserStory
)
Based on this model here’s a Cypher query that will add a sample set of data to Neo4J:
1// Create Product2CREATE (ChocolateBox:Product {id: 'PRODUCT-1', name:'Chocolate Box'})3
4// Create Figma mockups5CREATE (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 Stories10CREATE (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 Cases14CREATE (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 Product22CREATE (ViewFlowersList)-[:DEFINES {author: 'Colin'}]->(ChocolateBox)23CREATE (ViewChocolatesList)-[:DEFINES {author: 'Not Colin'}]->(ChocolateBox)24
25// Link Mockups to Story26CREATE (LandingPage)-[:VISUALISES]->(ViewFlowersList)27CREATE (LandingPage)-[:VISUALISES]->(ViewChocolatesList)28CREATE (ChocolatePLP)-[:VISUALISES]->(ViewChocolatesList)29CREATE (FlowerPLP)-[:VISUALISES]->(ViewFlowersList)30
31// Link Tests to user story32CREATE (TD1)-[:TESTS]->(ViewFlowersList)33CREATE (TD2)-[:TESTS]->(ViewChocolatesList)34CREATE (TD3)-[:TESTS]->(ViewChocolatesList)35CREATE (TD4)-[:TESTS]->(ViewChocolatesList)36CREATE (TD5)-[:TESTS]->(ViewChocolatesList)37CREATE (TD6)-[:TESTS]->(ViewChocolatesList)
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! @id3 name: String4 }5 type Mockup {6 id: ID! @id7 name: String8 nodeId: String9 }10 type UserStory {11 id: ID! @id12 name:String13 }14 type Test {15 id: ID! @id16 name: String17 }
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 id4 name5 }6 mockups {7 id8 name9 }10 userStories {11 id12 name13 }14 tests {15 id16 name17 }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"""
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! @id3 name: String4 userStories: [UserStory!]! @relationship(type: "DEFINES", properties: 'Defines', direction: IN)5 }6 type Mockup {7 id: ID! @id8 name: String9 nodeId: String10 userStories: [UserStory!]! @relationship(type: "VISUALISES", direction: OUT)11 tests: [Test] @cypher(12 statement: """13 MATCH (this)-[:VISUALISES]->(story)<-[:TESTS]-(test)14 RETURN distinct test15 """16 )17 }18 type UserStory {19 id: ID! @id20 name:String21 mockups: [Mockup!]! @relationship(type: "VISUALISES", direction: IN)22 tests: [Test!]! @relationship(type: "TESTS", direction: IN)23 }24 type Test {25 id: ID! @id26 name: String27 lastResult: String28 userStories: [UserStory!]! @relationship(type: "TESTS", direction: OUT)29 }30 interface Defines @relationshipProperies {31 author: String32 }
And an example query for fetching the relationships:
1query Query {2 products {3 name4 userStoriesConnection {5 edges {6 author7 node {8 name9 }10 }11 }12 }13 mockups {14 name15 tests {16 name17 }18 userStories {19 name20 }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"""
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 like3{4 "input": {5 "name": "A new test"6 }7}8"""9mutation Mutation($input: [TestCreateInput!]!) {10 createTests(input: $input) {11 tests {12 id13 }14 }15}16"""17Response looks like18{19 data: {20 createTests: {21 tests: [22 {23 id: 'ID of the newly created test'24 }25 ]26 }27 }28}29"""
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 like3{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 name16 }17 }18}19"""20Response looks like21{22 data: {23 updateTests: [24 {25 name: 'Updated test'26 }27 ]28 }29}30"""
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 like3{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 name26 userStories {27 name28 }29 }30 }31}32"""33Response looks like34{35 data: {36 updateTests: [37 {38 name: 'Updated test',39 userStories: [40 {41 name: 'Linked User Story'42 }43 ]44 }45 ]46 }47}48"""
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 like3{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 name26 userStories {27 name28 }29 }30 }31}32"""33Response looks like34{35 data: {36 updateTests: [37 {38 name: 'Updated test',39 userStories: []40 }41 ]42 }43}44"""
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 like3{4 "where": {5 "id": "ID of the node to delete"6 }7}8"""9mutation Mutation($where: TestWhere) {10 deleteTests(where: $where) {11 nodesDeleted12 relationshipsDeleted13 }14}15"""16Response looks like17{18 "data": {19 "deleteTests": {20 "nodesDeleted": 1,21 "relationshipsDeleted": 022 }23 }24}25"""
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: