The goal of this guide is to explain how to design your GraphQL schema for use with GRANDstack and the Neo4j GraphQL Library. Refer to the Neo4j GraphQL Library documentation for more information.
Three Principles of the Neo4j GraphQL Library
It's important to understand the goals and advantages of using Neo4j GraphQL.
- GraphQL type definitions drive the Neo4j database schema.
- A single Cypher database query is generated for each GraphQL request.
- Custom logic can be added with
@cypherGraphQL schema directives to extend the functionality of GraphQL beyond CRUD.
GraphQL Type Definitions Drive The Database
A major benefit of using the Neo4j GraphQL Library is the ability to define both the database schema and the GraphQL schema with the same type definitions. The Neo4j GraphQL Library can also automatically create
Mutation types from your type definitions that provide instant CRUD functionality. This GraphQL CRUD API can be configured and extended.
A Single Cypher database query is generated for each GraphQL Request
Since GraphQL type definitions are used to define the database schema, the Neo4j GraphQL Library can generate a single Cypher query to resolve any arbitrary GraphQL request. This also means our resolvers are automatically implemented for us. No need to write boilerplate data fetching code, just inject a Neo4j driver instance into the request context and the Neo4j GraphQL Library will take care of generating the database query and handling the database call. Additionally, since a single Cypher query is generated, this results in a huge performance boost and eliminates the n+1 query problem.
Defining Custom Logic With
@cypher Schema Directives
Since GraphQL is an API query language and not a database query language it lacks semantics for operations such as aggregations, projections, and more complex graph traversals. We can extend the power of GraphQL through the use of
@cypher GraphQL schema directives to bind the results of a Cypher query to a GraphQL field. This allows for the expression of complex logic using Cypher and can be used on Query and Mutation fields to define custom data fetching or mutation logic.
For more on how the Neo4j GraphQL Library aims to solve some of the common problems that come up when building GraphQL APIs see this post.
What is a GraphQL schema?
A GraphQL schema defines the types and the fields available on each type, which can include references to other types. A GraphQL schema includes two special types
Mutation, the fields of which define the entry points for the schema. In typical GraphQL implementations the
Mutation types must be defined by the developer, however in our case the Neo4j GraphQL Library will handle generating CRUD queries and mutations based on our type definitions. We do however have the ability to define custom queries and mutations by either using a
@cypher schema directive in our type definitions, effectively annotating the schema with a Cypher query, or implementing the resolver in code. See below for examples of each of those approaches.
The official guide to GraphQL makes the observation that your application data is a graph. GraphQL allows you to model your business domain as a graph, which in the client presents an object-oriented like approach to data where objects reference other types, creating a graph. However, as GraphQL is data layer agnostic, the guide says developers are free to implement the backend however they wish. When GraphQL is used with a graph database such as Neo4j, we don't need a mapping and translation layer to translate our datamodel, instead the GraphQL type definitions can drive the database data model. The graph model of GraphQL translates easily to the labeled property graph model used by Neo4j and other graph database systems.
Nodes, relationships, and properties.
In a graph database nodes are the entities in the graph and relationships connect them. We can store arbitrary key-value pairs as properties on both nodes and relationships. This is the labeled property graph model.
In GraphQL we define types and the fields available on each type, which may reference other types. For example:
How then do we map the GraphQL type system to the labeled property graph? By applying these basic rules:
- GraphQL type names become node labels
- Fields become node properties
- Reference fields become relationships (we discuss the case of modeling relationship types below)
The GraphQL Schema Definition Language
In GraphQL we use the Schema Definition Language (SDL) to define our types in a language agnostic way.
We use SDL to define object types for our domain and the fields available on each type, which may be scalar types or reference other object types. For example:
This type definition is defining a node with the label
Movie and the properties
Our type definitions can reference other object types as well:
Fields can include arguments. For example, we may want to limit the number of actors we return for movie:
limit is an integer argument with default value 10. This argument can be specified at query time.
Schema directives are GraphQL's built-in extension mechanism. We can use them to annotate fields or object type definitions.
@relationship Schema Directive
To be able to fully specify the labeled property graph equivalent we make use of the
@relationship GraphQL schema directive to declare the relationship type and direction:
direction is an enum added to our type definitions by the Neo4j GraphQL Library with possible values IN, OUT.
These type definitions then map to this labeled property graph model in Neo4j:
@cypher Schema Directive
The Neo4j GraphQL Library introduces a
@cypher GraphQL schema directive that can be used to bind a GraphQL field to the results of a Cypher query. For example, let's add a field
similarMovies to our
Movie which is bound to a Cypher query to find other movies with an overlap of actors:
In the context of the Cypher query used in a
thisis bound to the currently resolved object, similar to the
objectparameter passed to the GraphQL resolver, in this example
thisbecomes the currently resolved movie.
GraphQL field arguments for the
@cypher directive field are passed to the Cypher query as Cypher parameters (In this case
@cypherdirectives can be used to implement authorization logic as well. We can include values from the request context, such as those added by authorization middleware as Cypher parameters. See the auth documentation for more information.
In typical GraphQL server implementations we would now define a
Mutation object type, the fields of each becoming the entry points for our API. With the Neo4j GraphQL Library we don't need to define a
Mutation type as queries and mutations with full CRUD operations will be generated for us from our type defintions.
We also don't need to define resolvers - the functions that contain the logic for resolving the actual data from the data layer. Since the Neo4j GraphQL Library handles database query generation and data fetching our resolvers are generated for us. You can read more about the API generation process in the Schema section of the documentation.
Custom Queries And Mutations
The auto-generated API gives us basic CRUD functionality, but what if we wanted to implement our own custom logic? We have two options for implementing custom logic with the Neo4j GraphQL Library:
- Using the
- Implementing custom resolvers
To implement a custom
Query field, simply include a
Query type in your type definitions and include your custom logic in a
@cypher directive annotated field. Here we use a fulltext index to perform a fuzzy text match for searching for movies:
This approach can be used to define custom logic for Mutation fields as well. See the documentation for more examples.
Implementing Custom Resolvers
Resolvers are functions that define how to actually fetch the data from the data layer. Because the Neo4j GraphQL Library autogenerates CRUD resolvers for queries and mutations, you don't need to implement resolvers yourself, however if you have some custom code beyond which can be defined using an
@cypher directive, you can implement your own resolvers and pass those along. Here's a simple example defining a field resolver for