DEV Community


Quick prototyping with GRAND stack – part 2

Love building products
Updated on ・4 min read

Quick prototyping with GRAND stack: part 2

  • Part 1 – Product introduction, tech spec and graph model.
  • Part 2 - Apollo Graphql server and neo4j-graphql-js
  • Part 3 - Apollo client and React

In the first part of the series we introduced our product, our graph for the tech spec and discussed why you should consider GRAND stack for quick prototyping. In this part I will show you how to quickly build a graphql server using apollo server and neo4j-graphql-js. This post assumes some familiarity with graphql server setups.

Graphql server with neo4j-graphql-js

The main benefit of neo4j-graphql-js is that it delegates writing your own graphql resolvers to cypher queries. It makes neo4j the work horse of your business logic. Additionally, it takes care of pagination, filtering and the dreaded N+1 query problem. Let's take a look at an example – our type definitions in schema.graphql:

type User {
  userId: ID!
  name: String
  email: String!
  matchCandidates: [UserWithScore!]!
      statement: """
      MATCH (this)-[:PREFERS]->()<-[:PREFERS]-(candidate:User)
      WHERE NOT EXISTS((this)-[:LIKES|:DISLIKES]->(candidate))
      WITH candidate, COUNT(*) AS score ORDER BY score DESC
      RETURN {userId:candidate.userId, score:score}
  matchedWith: [Match!]! @relation(name: "MATCHED", direction: "OUT")

type Match {
  matchId: ID!
  date: Date!
  createdAt: Date!
  users: [User!]! @relation(name: "MATCHED", direction: "IN")
  tracks: [Track!]! @relation(name: "HAS", direction: "OUT")

type Mutation @isAuthenticated {
  like(from: ID!, to: ID!): LikeResult
      statement: """
      MATCH (n:User {userId:$from}),(o:User {userId:$to} )
      OPTIONAL MATCH (n)<-[r:LIKES]-(o)
      MERGE (n)-[:LIKES]->(o)
      WITH n,o,r
      'MERGE (n)-[:MATCHED]->(m:Match { matchId:apoc.create.uuid(),createdAt:datetime()})<-[:MATCHED]-(o) RETURN {matchId:m.matchId, matched:true,} as result',
      'RETURN {matched:false} as result',
      {n:n, o:o}
      ) YIELD value
      RETURN value.result
  addTrack(userId: ID!, matchId: ID!, type: String!, desc: String!): Track!
      statement: """
      MATCH (n:User {userId:$userId}), (m:Match {matchId:$matchId})
      MERGE (n)-[:ADDED]->(t:Track {trackId:apoc.create.uuid(), type:$type,description:$desc, matchId:m.matchId, createdAt:datetime()})
      MERGE (m)-[:HAS]->(t)
      return t
Enter fullscreen mode Exit fullscreen mode

Neo4j-graphql-js ships with a few helpful graphql schema directives that allow the above code:

  1. @cypher to write our neo4j queries directly in our typedefs.
  2. @relation to specify fields by leveraging our graph relations.
  3. @neo4j_ignore to specify a custom resolver.
  4. @isAuthenticated to provide authentication capabilities.

Our apollo server:

const resolvers = {
  Mutation: {
    like: async (obj, args, context, info) => {
      const result = await neo4jgraphql(obj, args, context, info);
      if ( {
      return result;
const server = new ApolloServer({
  schema: makeAugmentedSchema({
    config: {
      auth: { isAuthenticated: true },
  context: ({ req }) => ({
Enter fullscreen mode Exit fullscreen mode

In those 2 code blocks above we actually specified 90% of our api server. In the previous part we went through matchCandidates field on type User. Now, let's go over the like mutation line by line. First we see @neo4j_ignore, it let's us specify our custom resolver in the second code block for the purpose of adding logic not directly related to our graph (sending email in this case). Next is the cypher query. Line by line:

  1. Find two users by id – me and the user I like.
  2. Check if the other user already liked me.
  3. Create a like relation between me and other user.
  4. Collect variables specified in the above lines, me, other user and their potential like of me.
  5. Apoc procedure to do some branching.
    • Apoc is a library of many helpful procedures and algorithms to make our graph developer experience better.
    • In our procedure we check if the other user has already liked us, if they have we create a match and provide email in the result to notify the other user that we matched. Return false otherwise.

All the heavy lifting is done by makeAugmentedSchema, which auto-generates queries, mutations, filtering and pagination. Here is a great write up on how makeAugmentedSchema avoids N+1 query problem. Basically, it traverses the graph structure given in the resolveInfo object and combines every field's query as a subquery for one query on that type.


The key feature of GRAND stack is that – once you are finished with modelling your business requirements with a graph – to get your business logic layer set up is a matter of writing out a few type definitions with a declarative cypher query language, because graphs lend themselves quite naturally to a variety of complex requirements.

To better illustrate this point. Let's say we want to add a 3-way match requirement, i.e if Alice has a mutual like with Bob and Claire, and Bob and Claire have a mutual like between them, create a 3-way match card. This is how easily this requriment is satisfied on the backend:

type User{
    MATCH (u1:User)-[:MATCHED*2]-(this)-[:MATCHED*2]-(u3:User)
    WHERE EXISTS((u1)-[:MATCHED*2]-(u3))
    RETURN u1,u3
Enter fullscreen mode Exit fullscreen mode

In just 3 lines we satisfied a non-trivial requirement which would let us justify the following product slogan: "For every Harry we will help find Hermione and Ron".

Our graph

match and hack graph

The productivity of setting up graphql servers with neo4j is remarkable. I got a working server for in 2 hours after fiddling with the arrow tool and making my graph (see the image above). In the next part we will see how apollo-client simplifies data management on the front-end to get us even closer to the LAMP stack level of productivity.

Discussion (0)