GRAND Stack: One schema to rule them all
In this article I will show you the power of the GRAND stack, for creating a web application on top of Neo4j where everything is typed, just by using your data schema.
Note
|
The code source of this article can be found on this gitlab repository
To run it, you must have a Neo4j database with the movies graph (ie. :play movie in the Neo4j browser).
And don’t forget to change the login/password in the file backend/src/config.ts .
|
The GRAND stack
Developed by Neo4j, the GRAND stack is made for creating web application with modern technologies. It’s composed of :
-
GraphQL for the API server
-
React for the web application
-
Apollo for the GraphQL client & server
-
Neo4j Database for the storage
It’s a mainly a GraphQL backend on top of Neo4j, and a single page application.
GraphQL & Neo4j, a great story
GraphQL considered that your data schema is a graph, and Neo4j is a graph database. So there is a perfect match between both.
Neo4j has developed a library called neo4j-graphql-js that do the glue between GraphQL & Neo4,j and it’s pretty powerful.
It can generate for you all your GraphQL resolvers just by adding schema directives
What I love also, it’s that we avoid the N+1 problem in GraphQL : one GraphQL query equals to one Cypher query. So we have performances!
But the library can do more for you, it can generate the GraphQL schema but also the Neo4j schema. Let’s see that.
Generate the schema from the Neo4j’s one
neo4j-graphql-js comes with the function inferSchema that generates the GraphQL schema directly from a Neo4j database.
This code generates the GraphQL schema from the Neo4j one, and displays it in the console:
import { inferSchema } from "neo4j-graphql-js";
import neo4j from "neo4j-driver";
import { config } from "../src/config";
// create the neo4j driver
const driver = neo4j.driver(config.neo4j.url, neo4j.auth.basic(config.neo4j.login, config.neo4j.password));
// infer the graphql schema from neo4j
inferSchema(driver).then((result) => {
console.log(result.typeDefs);
process.exit();
});
For the Neo4j movies graph, the result is:
type Person {
_id: Long!
born: Int
name: String!
acted_in: [Movie] @relation(name: "ACTED_IN", direction: OUT)
ACTED_IN_rel: [ACTED_IN]
directed: [Movie] @relation(name: "DIRECTED", direction: OUT)
produced: [Movie] @relation(name: "PRODUCED", direction: OUT)
wrote: [Movie] @relation(name: "WROTE", direction: OUT)
follows: [Person] @relation(name: "FOLLOWS", direction: OUT)
reviewed: [Movie] @relation(name: "REVIEWED", direction: OUT)
REVIEWED_rel: [REVIEWED]
}
type Movie {
_id: Long!
released: Int!
tagline: String
title: String!
persons_acted_in: [Person] @relation(name: "ACTED_IN", direction: IN)
persons_directed: [Person] @relation(name: "DIRECTED", direction: IN)
persons_produced: [Person] @relation(name: "PRODUCED", direction: IN)
persons_wrote: [Person] @relation(name: "WROTE", direction: IN)
persons_reviewed: [Person] @relation(name: "REVIEWED", direction: IN)
}
type ACTED_IN @relation(name: "ACTED_IN") {
from: Person!
to: Movie!
roles: [String]!
}
type REVIEWED @relation(name: "REVIEWED") {
from: Person!
to: Movie!
rating: Int!
summary: String!
}
Pretty cool, isn’t it? Generally I don’t use it this way, I modify it a little by:
-
removing the
_id
(never use the internal id of Neo4j, it’s a bad practice) -
rename the properties for relationships
-
review the cardinality of relationships (tips: you can also use the directive
@relation
for a cardinality of1
)
In my package.json
, I have a task that run the piece of code above just by running npm run generate:schema
But you can directly use the generated schema as it is.
Generate Neo4j schema from the GraphQL’s one
You can also generate the Neo4j schema (ie. indexes, constraints) from the GraphQL schema.
Since version 2.16.0, neo4j-graphql-js comes with those directives:
-
@id
: to be used on primary key fields -
@index
: to be used on fields where you want to create an index -
@unique
: to be used on fields that should be unique
The @id
can be only used once per node, the library doesn’t support node keys.
And the @index
directive doesn’t support composite index. The directive creates one index per field.
Simple example :
type Person {
id: ID! @id
name: String! @index
hash: String! @unique
born: Date
}
Now that the definition is done, you need to apply this schema on Neo4j by calling assertSchema
like that :
import { Express } from "express";
import { Server } from "http";
import { ApolloServer } from "apollo-server-express";
import { makeAugmentedSchema, assertSchema } from "neo4j-graphql-js";
import neo4j from "neo4j-driver";
import { config } from "../config";
import { resolvers, typeDefs, config as gqlConfig } from "./schema";
export function register(server: Server, app: Express): void {
// create the neo4j driver
const driver = neo4j.driver(
config.neo4j.url,
neo4j.auth.basic(config.neo4j.login, config.neo4j.password)
);
// create the Neo4j graphql schema
const schema = makeAugmentedSchema({
typeDefs,
resolvers,
config: gqlConfig
});
// create the graphql server with apollo
const serverGraphql = new ApolloServer({
schema,
context: { driver }
});
// Register the graphql server to express
serverGraphql.applyMiddleware({ app });
// Sync the Neo4j schema (ie. indexes, constraints)
assertSchema({ schema, driver, debug: true });
}
This is the result (due to the debug: true
) :
┌─────────┬─────────────────┬─────────┬─────────────┬────────┬───────────┐
│ (index) │ label │ key │ keys │ unique │ action │
├─────────┼─────────────────┼─────────┼─────────────┼────────┼───────────┤
│ 0 │ 'Person' │ 'name' │ [ 'name' ] │ false │ 'CREATED' │
│ 1 │ 'Person' │ 'id' │ [ 'id' ] │ true │ 'CREATED' │
│ 2 │ 'Person' │ 'hash' │ [ 'hash' ] │ true │ 'CREATED' │
└─────────┴─────────────────┴─────────┴─────────────┴────────┴───────────┘
The assertSchema
synchronizes your GraphQL definition with the Neo4j schema.
For example, if you remove the @unique
on the hash
field and you re-run the script,
the result will be:
┌─────────┬──────────┬────────┬────────────┬────────┬───────────┐
│ (index) │ label │ key │ keys │ unique │ action │
├─────────┼──────────┼────────┼────────────┼────────┼───────────┤
│ 0 │ 'Person' │ 'name' │ [ 'name' ] │ false │ 'KEPT' │
│ 1 │ 'Person' │ 'id' │ [ 'id' ] │ true │ 'KEPT' │
│ 2 │ 'Person' │ 'hash' │ [ 'hash' ] │ true │ 'DROPPED' │
└─────────┴──────────┴────────┴────────────┴────────┴───────────┘
As you can see the unique constraint has been dropped!
React, TypeScript & GraphQL
If you want to build a React application where types matter, obviously you need TypeScript.
Ok, but we can go further in the types definition with GraphQL, and I will show you in the next sections. But first we need to initialize our React project.
Create the project
The easiest way to create a React project with TypeScript is to use the create-react-app template with TypeScript support like that :
$> npx create-react-app frontend --template typescript
Then we need to add GraphQL and Apollo to support GraphQL
$> npm install @apollo/client GraphQL
For the dependencies, that’s all, but we need to make some code
to create our GraphQL client (check the file src/graphql/client
).
import { ApolloClient, InMemoryCache } from "@apollo/client";
export const client = new ApolloClient({
uri: "http://localhost:4000/graphql",
cache: new InMemoryCache(),
});
Finally, you just have to wrap your application with the ApolloProvider in your index.tsx
:
import React from "react";
import ReactDOM from "react-dom";
import * as serviceWorker from "./serviceWorker";
import "./index.css";
import { App } from "./App";
// graphQl
import { ApolloProvider } from "@apollo/client";
import { client } from "./graphql/client";
ReactDOM.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>,
document.getElementById("root"),
);
serviceWorker.unregister();
At this step, you have a working React application that supports TypeScript and GraphQL.
Note
|
For more details on how to integrate Apollo to your React project, you can check this page. |
Generating types & hooks
To see the generation in action, we need to make some GraphQL code, so let’s continue our example based on the movies graph.
Some GraphQL code
As an example, I will do a simple query that retrieves all the actors, and the movies they played in.
First, I create a GraphQL fragment for each model:
import gql from "graphql-tag";
import { DocumentNode } from "graphql";
export const fragments: { [name: string]: DocumentNode } = {
movie: gql`
fragment Movie on Movie {
_id
title
tagline
released
}
`,
person: gql`
fragment Person on Person {
_id
name
born
}
`,
};
And then I can write my query:
import gql from "graphql-tag";
import { fragments } from "./fragments";
export const getActors = gql`
query GetActors {
actors: Person {
...Person
acted_in {
...Movie
}
}
}
${fragments.person}
${fragments.movie}
`;
Now we can see the cool part, the code generation.
Code Generation
Now I will show you how to generate your code from the GraphQL schema, queries & fragment.
To do that, I use graphql-codegen. Let’s install all the dependencies:
$> npm install \
@graphql-codegen/cli \
@graphql-codegen/typescript \
@graphql-codegen/typescript-graphql-files-modules \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-react-apollo
And create the following task in the package.json
, so the code generation will be performed with npm run generate:types
:
...
"scripts": {
...
"generate:types": "graphql-codegen",
}
...
The last point is to create the configuration file for graphql-codegen.
At the root of the React project, you must have a file called codegen.xml
with the following content:
schema: http://localhost:4000/graphql
documents: ["src/graphql/**/*.ts"]
generates:
./src/graphql/types.tsx:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
config:
withHooks: true
avoidOptionals: true
Note
|
For more details on the configuration, check this page. |
Some explanations :
-
schema: http://localhost:4000/GraphQL
: defines the url of your GraphQL endpoint, so the generator can retrieve your GraphQL schema. It also means that your backend must be running to generate your code. -
documents: ["src/graphql/*/.ts"]
: defines the locations where the code generator can find your GraphQL queries and fragments. -
generates
: defines how and where the code will be generated. For the where, it’s in the file./src/graphql/types.tsx
. For the how, I have defined three plugins:-
typescript for the TypeScript support
-
typescript-operations to generate types based on the GraphQL operations (queries, mutations, inputs, variables, etc)
-
typescript-react-apollo to generate GraphQL React hooks for Apollo
-
Now you can generate the types:
$> npm run generate:types
> frontend@0.1.0 generate:types /home/bsimard/worspaces/ouestware/grand-stack-example/frontend
> graphql-codegen
✔ Parse configuration
✔ Generate outputs
Let see the generated code int the file src/graphql/types
.
The generated code
From your GraphQL schema
The generator do a lot of works on your schema, you will find types for:
-
GraphQL types (in our case
Movie
&Person
) -
GraphQL inputs and variables for your queries and mutations
-
the definition of your queries and mutations (search for
export type Mutation = {
orexport type Query = {
)
As an example, we will take a look at the Movie
type:
export type Movie = {
__typename?: 'Movie';
_id: Maybe<Scalars['String']>;
released: Scalars['Int'];
tagline: Maybe<Scalars['String']>;
title: Scalars['String'];
persons_acted_in: Maybe<Array<Maybe<Person>>>;
persons_directed: Maybe<Array<Maybe<Person>>>;
persons_produced: Maybe<Array<Maybe<Person>>>;
persons_wrote: Maybe<Array<Maybe<Person>>>;
persons_reviewed: Maybe<Array<Maybe<Person>>>;
};
It’s the exact translation of your type from your GraphQL schema.
From your GraphQL code (queries, fragment, …)
The generator parses also your front-end code, so it knows your queries, fragments, …
For each fragment, you will find a type called ${my_fragment_name}Fragment
.
In the code we have defined a fragment named Movie
, so let’s take a look at MovieFragment
:
export type MovieFragment = (
{ __typename?: 'Movie' }
& Pick<Movie, '_id' | 'title' | 'tagline' | 'released'>
);
And the best part is the generation of the React hooks.
For each query (or mutation), you will find a hook called use${my_query_name}Query
.
In the code we have defined a query named GetActors
, so let’s take a look at useGetActorsQuery
:
export function useGetActorsQuery(baseOptions?: Apollo.QueryHookOptions<GetActorsQuery, GetActorsQueryVariables>) {
return Apollo.useQuery<GetActorsQuery, GetActorsQueryVariables>(GetActorsDocument, baseOptions);
}
// for reference
export type GetActorsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetActorsQuery = (
{ __typename?: 'Query' }
& { actors: Maybe<Array<Maybe<(
{ __typename?: 'Person' }
& { acted_in: Maybe<Array<Maybe<(
{ __typename?: 'Movie' }
& MovieFragment
)>>> }
& PersonFragment
)>>> }
);
As you see, everything is typed (result, variables, options, …).
How to use the generated code
You just have to use the generated hook, like a normal Apollo hook. Here’s an example:
import React from "react";
import { useGetActorsQuery } from "./graphql/types";
import { ActorBox } from "./ActorBox";
export const ActorsList: React.FC = () => {
// Loading the data
const { data, loading, error } = useGetActorsQuery({ variables: {} });
return (
<>
<h1>Actors</h1>
{loading && <p>Loading ...</p>}
{error &&
error.graphQLErrors.map((e) => {
return <p>e.message</p>;
})}
{data?.actors &&
data.actors.map((actor) => {
return <ActorBox actor={actor} />;
})}
</>
);
};
What I also like is to use the fragments in my simple components that just display the item:
import React from "react";
import { PersonFragment, MovieFragment } from "./graphql/types";
import { MovieBox } from "./MovieBox";
interface Props {
actor: (PersonFragment & { acted_in: Array<MovieFragment | null> | null }) | null;
}
export const ActorBox: React.FC<Props> = (props: Props) => {
const { actor } = props;
if (actor === null) return null;
return (
<div className="actor">
<h2>
{actor.name} - ({actor.born})
</h2>
<div className="actor-movies">
{actor.acted_in?.map((movie) => {
return <MovieBox key={movie?._id} movie={movie} />;
})}
</div>
</div>
);
};
Note
|
You can take a look at the full code source of this React application here. |
If you run the code, you should see this result:
Conclusion
Here we have a robust stack where every layers have types, and where they are propagated from the database to the front-end. That comes with a lot of advantages:
-
fast development, thanks to code generation (from neo4j-graphql-js & graphql-codegen)
-
every one talk about the same schema
-
we have a strong interface between all the layers
-
auto-completion in IDE with types checking
-
data refactoring is easy, we directly see the impacts at the compilation