top of page
  • 5.15 Technologies

How to Build a Full Application Development Stack with Django

Introducing the DANG stack, Django, Angular, Neo4j, and GraphQL. In this multi-part series, we will show you how to build an application using one of the application development stacks used by our team. In this blog, we will demonstrate how to set up a functioning GraphQL API using Django and Neo4j.  In the second part, we'll lead you in creating an Angular application, powered by Django, which interacts seamlessly with our API.


As Django-centric developers, our primary method for data exchange between the backend and frontend involves RESTful APIs, utilizing JavaScript's fetch statement. Despite this, we frequently encounter the need to create new views whenever a new action, data source, or backend fetch operation is required.


REST APIs are inflexible and often force the user to either query multiple endpoints or retrieve more information than needed for a given task. This results in making many network requests or requiring a brand-new controller to be made specifically for the use case at hand. GraphQL APIs are flexible and can return exactly what information is needed by the request.


The solution to this problem is to implement GraphQL on a single endpoint while still being able to utilize Django’s powerful authentication and security features. Using Neomodel will allow us to natively manipulate graph data from Neo4j within Django to serve our endpoint seamlessly as if we were using standard Django models. This will result in a simpler API configuration that is more flexible, easier to use, and less resource intensive.


The repository for this project is located here.

Architecture

Neo4j Architecture Diagram
Figure 1. Architecture Diagram

The Neo4j database “serves” data that our Django application will structure via Neomodel. The models defined using Neomodel are used in the Graphene schema to structure queries and mutations for manipulating data in the Neo4j database. Django exposes the queries and mutations defined by the schema through a single URL. This URL is accessible via a web browser as the GraphiQL environment or as a standard GraphQL API endpoint.


Prerequisites

This blog assumes a basic knowledge of Django and Neo4j. Although we’re using JetBrains’ PyCharm IDE, any Python IDE will work just fine. You can download PyCharm here.


Neo4j Setup

 First, we’ll begin by loading our dataset into Neo4j via Neo4j Desktop. For simplicity’s sake, the sample movies dataset provided by Neo4j will be used.


If you have not used Neo4j Desktop before, you can download it here. Once it is downloaded and installed, create a new project.



Next, create a new DBMS to contain and serve the data.


Once your DBMS is created, click “Start” to start it up. 


Once the DBMS is running, open the Neo4j Browser, either in a web browser or through the Neo4j Desktop application, and execute the “:play movies” command.


Click the play button on the second page of results to execute the relevant queries that populate your database.


Once done, you should see Movie and Person nodes within your database like below.

Now that our database is populated, keep this DBMS running and shift your focus back to your IDE window. Now we can build our API.

 

Django Install

Let’s start out by creating a virtual environment to isolate our dependencies. In your environment, run “python -m venv venv”


This will create a virtual environment folder named “venv” in your directory.

To activate the virtual environment:


  • Windows users: “./venv/Scripts/activate”

  • Linux/macOS users: “./venv/bin/activate”

Once we activate this in our project, we’ll need to install the required packages:

pip install Django neomodel python-dotenv neo4j graphene-django

Now that the requirements are installed, create a new Django project with “django-admin startproject DANG.”

Next, navigate into the created project folder then run “django-admin startapp gqlapi”. This project will only have a single application. Once this is done, your folder structure should look like the following:

Before we start writing code to handle GraphQL requests, we’ll write a .env file to hold our Neo4j authentication credentials securely.


At the root of the project, create a .env file with a DATABASE_URL variable, and set it equal to the following (presuming that you are using Neo4j locally at the default port 7687):


"bolt://<username>:<password>@localhost:7687"

 

 This variable may vary based on how or where you are hosting Neo4j.

Next, we’ll bind our application to the project by updating our project’s routes to include our “gqlapi” application and listing it in the project’s installed applications.

# DANG/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('gqlapi', include('gqlapi.urls'))
]

# DANG/settings.py
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'gqlapi',
    'graphene_django',
]
...

While we’re in the settings file, we will go ahead and install Graphene, and then configure Neomodel using the .env file we wrote earlier.

# DANG/settings.py
from pathlib import Path
from neomodel import config
import os
from dotenv import load_dotenv

load_dotenv()

config.DATABASE_URL = os.environ.get("DATABASE_URL")

Now our defined models will be able to access Neo4j without any further configuration. Since we’re not using the default SQLite database, we need to remove the database settings so as not to conflict with Neomodel.

 

Define Models using Neomodel

 Our database contains two node types, Movie and Person, and a series of relationships to define how the two interact and are related. We will observe the direction and properties of each node and relationship type to recreate them in Neomodel so that our application can query and structure data identically to our database.

 

Our models are defined in much the same way as Django; however, we use Neomodel, an Object Graph Mapper (OGM) that helps us handle, parse, and make queries more easily to Neo4j in a “Djangonic” way. In our gqlapi application, create a new folder named models, from there, each new model will occupy its own Python file. Inside the “models” folder, create a new __init__.py and copy the below into it:

# gqlapi/models/__init__.py
from .movie import Movie
from .person import Person

This will instruct Python to treat the “models” folder as a package and import the models we need more easily in the future. Additionally, within the folder, create an empty movie.py and person.py.

 

Starting with the Movie node, we see that it has three properties: the year of its release, tagline, and title. It also has multiple relationships pointing to it. These will be denoted in Neomodel as “RelationshipFrom” types since they are relationships from People nodes. It’s important that we preserve the direction of the relationship. By observing how the relationships between Movie and Person nodes interact, we can see that there are more implicit properties or characteristics of Movie nodes than the defined properties.


For example, while there isn’t a property to define a list of those who acted in a Movie, we can see from the ACTED_IN relationship pointing from Person to Movie nodes that by collecting associated People nodes with this relationship, we are able to gather a list of actors for a given Movie. This way of thinking lets us define further properties for a given Movie constructed from its relationships with other data points.


# /gqlapi/models/movie.py
from neomodel import (
    StringProperty,
    IntegerProperty,
    ArrayProperty,
    StructuredNode,
    StructuredRel,
    RelationshipFrom,
)

class ActedIn(StructuredRel):
    roles = ArrayProperty(StringProperty())


class Reviewed(StructuredRel):
    rating = IntegerProperty()
    summary = StringProperty()


class Movie(StructuredNode):
    released = IntegerProperty()
    tagline = StringProperty()
    title = StringProperty()

    actors = RelationshipFrom('.person.Person', 'ACTED_IN', model=ActedIn)
    writers = RelationshipFrom('.person.Person', 'WROTE')
    producers = RelationshipFrom('.person.Person', 'PRODUCED')
    directors = RelationshipFrom('.person.Person', 'DIRECTED')
    reviewers = RelationshipFrom('.person.Person', 'REVIEWED', model=Reviewed)

We would like to draw your attention to the “ActedIn” and “Reviewed classes” as these are special since they are relationships with properties inherent to them. Since these relationships do more than just draw a relation between two datapoints, we need to specify models especially for them so we can access their attributes.


These are then referenced in the implicit properties we discussed above such as actors, writers, producers, etc. Now, we can look at the Person node which will function in a very similar way, albeit with different properties.


# gqlapi/models/person.py
from neomodel import (
    StringProperty,
    IntegerProperty,
    StructuredNode,
    RelationshipTo,
    RelationshipFrom,
)


class Person(StructuredNode):
    name = StringProperty()
    born = IntegerProperty()

    movies_acted_in = RelationshipTo('.movie.Movie', 'ACTED_IN')
    movies_written = RelationshipTo('.movie.Movie', 'WROTE')
    movies_produced = RelationshipTo('.movie.Movie', 'PRODUCED')
    movies_directed = RelationshipTo('.movie.Movie', 'DIRECTED')
    movies_reviewed = RelationshipTo('.movie.Movie', 'REVIEWED')
    followers = RelationshipFrom('Person', 'FOLLOWS')
    following = RelationshipTo('Person', 'FOLLOWS')

Since we defined our relationships with properties (StructuredRel) objects in Movie, we won’t implement that again in the Person model. This is partly to reduce the scope of the tutorial and make it easier to understand, but also because we don’t need to define those relationships for access both ways. If it’s defined on one model, it will be accessible from both when we make queries, this will be made clearer later.


Now that we’ve defined how we’ll be accessing our Person and Movie objects from Neo4j, we can integrate these models into our schema. A GraphQL schema defines the types and relationships between fields in our API; fields are the attributes of each object, or model, defined in the schema as well. The graphene-django package can extend Django model types natively, however, since we’re using Neomodel, we need to define these types manually using Graphene’s default “ObjectType”. “ObjectType” is the building block used to create Graphene objects.


In our schema we’ll define queries and mutations. Queries fetch data from our database, mutations change data. There are GraphQL subscriptions for maintaining a real-time connection to our database but that is outside the scope of this post.


GraphQL

Let’s start out our schema by importing Graphene and our models. Then we can define our models and StructuredRel objects as classes extending Graphene’s “ObjectType”. Since we define the StructuredRel objects as separate classes in our Movie model file, we need to do the same in our schema since they represent a more complex data structure than a standard relationship which will be a field otherwise.

# gqlapi/schema.py
import graphene
from .models import Movie, Person

class MovieType(graphene.ObjectType):
    released = graphene.Int()
    title = graphene.String()
    tagline = graphene.String()

    def resolve_released(parent, info):
        return parent.released

    def resolve_title(parent, info):
        return parent.title

    def resolve_tagline(parent, info):
        return parent.tagline


class ActedInType(graphene.ObjectType):
    roles = graphene.List(graphene.String)

    def resolve_roles(parent, info):
        return parent.roles


class ReviewType(graphene.ObjectType):
    rating = graphene.Int()
    summary = graphene.String()

    def resolve_rating(parent, info):
        return parent.rating

    def resolve_summary(parent, info):
        return parent.summary

class PersonType(graphene.ObjectType):
    born = graphene.Int()
    name = graphene.String()
    movies_acted_in = graphene.List(MovieType)
    movies_written = graphene.List(MovieType)
    movies_produced = graphene.List(MovieType)
    movies_directed = graphene.List(MovieType)
    movies_reviewed = graphene.List(MovieType)
    reviews_written = graphene.List(ReviewType)
    roles_played = graphene.List(ActedInType)

    def resolve_born(parent, info):
        return parent.born

    def resolve_name(parent, info):
        return parent.name

    def resolve_movies_acted_in(parent, info):
        return parent.movies_acted_in

    def resolve_movies_written(parent, info):
        return parent.movies_written

    def resolve_movies_produced(parent, info):
        return parent.movies_produced

    def resolve_movies_directed(parent, info):
        return parent.movies_directed

    def resolve_movies_reviewed(parent, info):
        return parent.movies_reviewed

    def resolve_reviews_written(parent, info):
        return [m.reviewers.relationship(parent) for m in parent.movies_reviewed]

    def resolve_roles_played(parent, info):
        return [m.actors.relationship(parent) for m in parent.movies_acted_in]

Let’s decipher what’s going on here. The MovieType class extends Graphene’s ObjectType class and defines how our queries and mutations will interact with Movie objects in Neo4j. The released, title, and tagline properties of the Movie objects are defined as Graphene integer and string objects. This is done as well for our ActedInType and ReviewType classes as well since those relationships contain properties as well.


Each class will contain a resolver function for each field, these functions are responsible for returning the data for a specific object attribute. The parent parameter represents the Neomodel object, and we return the attribute we defined in its models file to pass the information to our query. The info parameter returns meta-information and more about the query and will not be implemented here, visit this documentation for more information.


Next, we’ll define the mutations and queries we want to make available to our API. These will be predefined here in our code and available to the user like REST API endpoints. The difference is that instead of multiple REST API endpoints that each correspond to one action or piece of information, we will expose one endpoint that makes multiple queries and mutations available. We will see how this interacts later.

# gqlapi/schema.py
...
class CreateMovie(graphene.Mutation):
    class Arguments:
        released = graphene.Int(required=True)
        title = graphene.String(required=True)
        tagline = graphene.String(required=True)

    success = graphene.Boolean()
    movie = graphene.Field(lambda: MovieType)

    def mutate(self, info, **kwargs):
        movie = Movie(**kwargs)
        movie.save()

        return CreateMovie(movie=movie, success=True)

class CreatePerson(graphene.Mutation):
    class Arguments:
        born = graphene.Int()
        name = graphene.String()


    success = graphene.Boolean()
    person = graphene.Field(lambda: PersonType)

    def mutate(self, info, **kwargs):
        person = Person(**kwargs)
        person.save()

        return CreatePerson(person=person, success=True)


class Mutations(graphene.ObjectType):
    create_movie = CreateMovie.Field()
    create_person = CreatePerson.Field()
...

For this example, we have defined two mutations: CreateMovie and CreatePerson. These mutations create Person and Movie objects respectively and take their arguments as a subclass containing the required attributes of each object.


We then define a new Field of the data type we’re handling to return a representation of the data to the user. It’s important this is set as “graphene.Field()” whose lambda value is then the data type. The “mutate()” method acts like the resolver function used when defining the model classes above; the method sets our field object defined before it and sets its value equal to the value of each field in the Arguments class in “dict” form. Finally, we call “save()” on the object to save it in our database and we return the class with the new data we’ve created and a variable storing whether the query was successful.

 

The schema is made aware of our defined mutations in the Mutations class which simply stores each separate mutation as an attribute. Defining queries is much easier since we only have to pull data from our database instead of performing alterations therein.

# gqlapi/schema.py
...
class Query(graphene.ObjectType):
    all_movies = graphene.List(MovieType)
    person_by_name = graphene.Field(PersonType, name=graphene.String(required=True))
    person_by_year = graphene.Field(PersonType, year=graphene.Int(required=True))


    def resolve_all_movies(self, info):
        return Movie.nodes.all()

    def resolve_person_by_name(self, info, name):
        try:
            return Person.nodes.get(name=name)
        except Person.DoesNotExist:
            return None

    def resolve_person_by_born(self, info, year):
        try:
            return Person.nodes.get(born=year)
        except Person.DoesNotExist:
            return None


schema = graphene.Schema(query=Query, mutation=Mutations)

Our queries only need a single class Query to contain them all. We define queries by creating variables which store different data types or Fields, these will be the resulting format of each query:

  • “all_movies”: Defines a list of movies to be returned, will be resolved to return all movies in database.

  • person_by_name”: Defines a Graphene field that takes a string and will return a PersonType object. The query will be resolved to return a Person whose name matches the string provided.

person_by_year: Defines a Graphene field that takes an integer and will return a PersonType object. The query will be resolved to return a Person whose birth year matches the integer provided.


The resolver functions correspond to the variables above them and return data per interacting with Neomodel objects. For example, in resolve_all_movies() we invoke the Movie object directly calling Movie.nodes.all(). This method is from Neomodel and returns all Movie objects present within the database.


We wrap each resolver function’s logic in a try/catch statement if no information exists for the given query. In that case we return a “None object”, this is not necessary for resolve_all_movies() since no Movie objects in the database will be represented as an empty array.


Finally, at the end of our file we define our schema for export by calling graphene.Schema() which takes our Query and Mutations classes. Our endpoint will use this schema to provide queries and mutations to the user.Create GraphQL Endpoint

 

We’ll have to configure our app to recognize the schema we wrote. In the settings file:

# DANG/settings.py
...
GRAPHENE = {
    "SCHEMA": "gqlapi.schema.schema"
}
...

The last step is to configure a URL in our app’s urls.py file:

# gqlapi/urls.py
from django.urls import path
from graphene_django.views import GraphQLView
from django.views.decorators.csrf import csrf_exempt

urlpatterns = [
    path("", csrf_exempt(GraphQLView.as_view(graphiql=True)))
]

Here, we do a couple of things:


We set our path to be CSRF exempt so that we can test the endpoint from an API client such as Postman or Insomnia without being authenticated with Django.


Then, graphiql is set to true in our path so we can access a built-in GraphQL sandbox in the browser. Testing our queries can be done via either an API client or the browser as I’ll demonstrate shortly.


Our GraphQL endpoint will now be available at http://localhost:8000/gqlapi by default, or whichever port you chose.


Test GraphQL Queries

When we run our Django application and navigate to the above URL, GraphiQL should appear which gives us a sandbox environment from which to execute queries and see which ones are available.

The top left icon on the page shows automatically generated documentation for the queries available.

Here we can see that each of our data types defined in our schema are available here. We can explore the queries available to us by clicking the explorer icon.

In this view, all defined queries and mutations appear on the left sidebar. In this instance, I selected the “allMovies” query. You’ll notice that we are able to choose exactly which fields are present in our results which gives us very granular control over what data we’re pulling from the database and a lot of flexibility within even a single query when compared to REST.GraphQL query results are returned in JSON format in a series of JSON objects. Let’s try running a mutation:


Here, we run the createPerson mutation to make a new Person object in our database. In this instance, I create a Person whose name is “Cole” and who was born in 2000. This Person did not exist prior to running this mutation. In our results we see that the “success” field we defined comes back as true, and we “return” the Person that we created and their birth year.


If we look back in our database and query on the parameters of the person we created, you’ll find a new Person object!


Stay Tuned: The “A” in DANG!

Now that our GraphQL API is up and running with some basic queries and mutations, try writing some of your own to interact with the database such as creating mutations to link Person objects to Movies via relationships or even manipulating other databases. In Part 2, we’ll create an Angular application to act as a front-end to our API to utilize and visualize its results.

We trust you've relished this blog, and we encourage you to stay tuned for the upcoming Part 2.

 

If you're interested in leveraging 5.15's expertise in crafting Full Stack Applications or if you have a project in mind, we're here for you. Feel free to reach out and explore how 5.15's services can bring your vision to life. Contact us today, and let's turn your ideas into reality! 😊




bottom of page