The goal of this guide is to explain how to design your GraphQL schema for use with GRANDstack and neo4j-graphql.
While the examples in this guide are specific to neo4j-graphql.js the same concepts apply to the other implementations of neo4j-graphql. We also discuss some best practices for schema design and common patterns for considerations such as implementing custom logic.
Three Principles of Neo4j GraphQL
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 neo4j-graphql is the ability to define both the database schema and the GraphQL schema with the same type definitions. neo4j-graphql 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, neo4j-graphql 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 neo4j-graphql 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 neo4j-graphql 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 neo4j-graphql.js 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 annotation 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.
@relation Schema Directive
To be able to fully specify the labeled property graph equivalent we make use of the
@relation GraphQL schema directive to declare the relationship type and direction:
direction is an enum added to our type definitions by neo4j-graphql with possible values IN, OUT.
These type definitions then map to this labeled property graph model in Neo4j:
@cypher Schema Directive
neo4j-graphql implementations introduce 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 authorization guide for more information.
In the case above we model properties on nodes, but not on relationships. To handle the case where we want to model properties on relationships we promote the reference field to an object type and annotate it with the directive
@relation. For example, let's introduce users and ratings:
An object type definition that includes an
@relation directive will be treated as a relationship. We use the convention that the fields
to define the nodes on the outgoing and incoming ends of the relationship, respectively, thus resulting in the following model in Neo4j:
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 neo4j-graphql 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 neo4j-graphql handles database query generation and data fetching our resolvers are generated for us as part of the schema augmetation process.
Mutation types would be generated for our type definitions above, defining CRUD operations for all our types:
You can read more about the API generation process in the neo4j-graphql.js user guide.
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 neo4j-graphql:
- 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.
Implementing Custom Resolvers
Resolvers are functions that define how to actually fetch the data from the data layer. Because neo4j-graphql.js 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