Angular With @neo4j/graphql and the Nest.js Backend


neo4j cypher query showing movies and actors
neo4j cypher query showing movies and actors

In my previous article, you could learn how to set up @neo4j/graphql with the NestJS backend framework. So now let’s create an Angular frontend on top of that and organize everything with NX monorepo.

I will try to show how quickly you can start building an entire app with @neo4j/graphql used on the backend.

Initialization

You can start by cloning the repo and following step by step along this tutorial. Before installing, just make sure you create your .env file and set up USE_AUTH to 0 to skip AWS Cognito authentication.

The starter app you can find in the repository, on the branch called “blank”. When you clone the project and hit npm install, you’ll have:

  • NestJS backend with movie
  • Neo4j database (docker)
  • Angular front-end app

Questions to Answer

We can use one of the datasets that Neo4j provides, for example, a movie database. Let’s think for a second: What problems do we want to resolve with our system? Well, we would like to inform our users about some movies, by providing data for the pages below:

  • A page with a list of all available movies with a search (filter list of movies by actors and/or year)
  • A movie page with all its data ( producer, director, and all the actors)
  • A page to list a person’s data (movies she/he acted in, directed, and the movies produced

Prepare the Backend

To populate the database with sample movie data, just go to Neo4j browser (you can either open localhost:7474 if you are running a local database, or try our online hosted version at browser.neo4j.io and run the :play movie-graph command.

GraphQL Models

As you may already know, Neo4j is built with nodes (data pieces) and relationships (how data pieces are related to each other). Nodes can have labels (to describe the data type). Both nodes and relationships can have properties to provide more information. That’s it.

In our example, we have nodes described as Person and Movie. The relations describe how they are connected.

# Nodes and Relationships
node:Person
node:Movie
relation:ACTED_IN
relation:DIRECTED
relation:PRODUCED

# Relationships directions
Person -DIRECTED-> Movie
Person -PRODUCED-> Movie
Person -ACTED_IN-> Movie

Along with some properties describing movies and people, we have basic tools to describe our data.

You can find the model definition in the file libs/gql/src/lib/type-defs.ts ;. If you know how to read GraphQL, you probably understand what’s there. Otherwise, let’s go through it.

There are two types defined, Movie and Person. Each type contains some basic fields — Person: name, born and Movie: title, released, and tagline. Additionally, they have fields described with a @relationship directive — we use it for fields defined by relationships. For example, actors are defined by ACTED_IN relationship, and we can create a field that will represent all Person nodes with a relationship of type ACTED_IN. Additionally, our relationship has its own properties defined in the ActedIn interface. It contains a list of Person roles in the given movie. As in the snippet below:

// type-defs.ts
import { gql } from 'apollo-server-express';

export const typeDefs = gql`
type Person {
name: String!
born: Int
actedIn: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT)
directed: [Movie!]! @relationship(type: "DIRECTED", direction: OUT)
produced: [Movie!]! @relationship(type: "PRODUCED", direction: OUT)
}

type Movie {
title: String!
released: Int!
tagline: String
actors: [Person!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN)
directedBy: [Person!]! @relationship(type: "DIRECTED", direction: IN)
producedBy: [Person!]! @relationship(type: "PRODUCED", direction: IN)
}

interface ActedIn @relationshipProperties {
roles: [String!]
}
`;

Building Angular Application

Since we are building our application in the NX monorepo, let’s put our functionality in the movie library.

nx generate @nrwl/angular:library --name=movies --style=scss --addModuleSpec --changeDetection=OnPush --importPath=movies --lazy --routing --simpleModuleName --standaloneConfig

Inside the created library, we will create a MovieModule that will contain three pages, plus a few components, routing and services. We will focus only on the GraphQL part, and we skip the entire components creation part. You can check in the repo how they are implemented. I just quickly describe what is where:

movies/
components/ // presentational components
pages/ // this contains page components
queries/ // graphql queries and mutations
services/ // business logic
types/ // types used in our module, hopefully self-describing
lib.routes.ts // routes definitions

The entire codebase for this blog post can be found on the branch movies-graphql (link). All links can be found at the bottom of the article.

So the database is populated, and components are ready, but if you click through, you’ll quickly see that the data is static. So, let’s jump into the code to connect the real data. We need to build queries and use them in our service to fetch the data and we’re good.

Create Queries

We need to create three queries for each page: movie list, movie details, and person details. Let’s start with the easiest one:
We want to display a movie title and its tagline. Here is the query we can try in our sandbox in localhost:3333/graphql. It will return a list of all node movies with properties title and tagline from the database.

query {
movies {
title
tagline
}
}

Let’s move that in a typescript file so we will be able to import it whenever we need it:

import { gql } from "@apollo/client/core"; 
export const GET_MOVIES = gql(`
query {
movies {
title
tagline
}
}
`);

The last thing is to replace the getAllMovies() method in the MovieService:

@Injectable({providedIn: "root"}) 
export class MovieService {

constructor(private readonly apollo: Apollo) {
}

getAllMovies(): Observable<MovieListItem[]> {
return this.apollo
.watchQuery<any>({
query: GET_MOVIES,
})
.valueChanges.pipe(
map(response => {
return response.data.movies;
})
);
}

That was easy. Now, if we click on the person’s name (either actor, director, or producer), we will need to make a query based on the person’s name. Assuming we get the name from the URL, the GraphQL query would look like this:

query { 
people(where: {name: "Tom Hanks"}) {
name
born
actedIn {
title
}
directed {
title
}
produced {
title
}
}
}

Take a look at the related items. There are relationships mapped to properties and combined into arrays: actedIn, directed, produced — as we defined in the type definition file.

Now, let’s transform that into a format that will work in the application. We should also update the query to provide the name parameter, making the query more flexible.

import { gql } from "@apollo/client/core";

export const GET_PERSON_DETAILS = gql`
query PersonDetails($name: String) {
people(where: {name: $name}) {
name
born
actedIn{
title
}
directed{
title
}
produced{
title
}
}
}
`;

This should go in the service MovieServive under the getPersonByName method:

getPersonByName(name: string) {
return this.apollo
.watchQuery<any>({
query: GET_PERSON_DETAILS,
variables: {
name: name
},
})
.valueChanges.pipe(
map(response => {
const {data } = response;
return data.people[0];
}),
);
}

Since our result is an array, we want to take only the first item — that is handled in the mapping function. This service method is called in the page component.

For the last query, we want to get the full movie info, including the actors and roles they played in the given movie. Since a role is described as a relationship property, the query will look a little bit different.

movies(where: {title: $title}) {
title
tagline
released

directedBy {
name
born
}

producedBy {
name
born
}

actorsConnection {
edges {
node {
name
}
roles
}
}
}

If we would want to display only actors, then we could choose the actors property. However, we’d like to display the roles that actors play, and the role is described in the properties of the ACTED_IN relationship.

The example below shows it’s the best:

(p:Person)-[a:ACTED_IN]->(m:Movie) WHERE m.title = ‘Cloud Atlas’

With our query, we display all (:Person) nodes that are connected with the (:Movie) node with a particular title, and in the right sidebar, we can see the properties of the selected [:ACTED_IN] relationship.

So we have four actors that acted in the movie, but each actor performed different roles.

Our GraphQL library allows for creating such queries that have relation properties.

We could get just actors, but that would not give us full information. This can be easily compared in the query below — in the actors property we want to get all actor names, but to get more, we have to reach actorsConnection field and define the edges (relationships in Neo4j terms), with its properties (roles) and additionally to have the context of the (:Actor) node property.

Take a look at the query below:

As you can see, you can get more context from the query.

Conclusion

Neo4j provides a flexible tool that can be set up for use in no time.

I have shown you only a tiny part of it, but it’s definitely worth taking a closer look at it since it gives you much more than just that.

With proper type configuration, you may create JWT authentication, role-based authorization, subscriptions, custom resolvers, and much more.

Links