GRAND Stack: One schema to rule them all

grand-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.

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 :

Itโ€™s a mainly a GraphQL backend on top of Neo4j, and a single page application.

grandstack_architecture

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

height=150

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 neo4j from "neo4j-driver";
import { inferSchema } from "neo4j-graphql-js";

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 of 1)

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

height=150

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

height=300

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:

// graphQl
import { ApolloProvider } from "@apollo/client";
import React from "react";
import ReactDOM from "react-dom";

import * as serviceWorker from "./serviceWorker";
import { App } from "./App";
import { client } from "./graphql/client";
import "./index.css";

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.

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

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:

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 = { or export 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>
  );
};


If you run the code, you should see this result:

height=400

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