Over the course of the coming weeks, I’ll be attempting to build a social site backend API using GraphQL. I’ll be covering some key concepts in GraphQL such as queries and mutations.

Prerequisites

I’ll strive to explain most if not all steps along the way, however, a basic understanding of the python programming language will go a long way in following along.

What is GraphQL?

According to the official website,

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data.

Some of the tenets that make up GraphQL include:

Data fetching:

Unlike with a REST-based API, with GraphQL you are able to fetch a comprehensive data collection without having to query multiple endpoints. You need only send a single query with the data requirements and get back just what you requested.

What you ask is what you get:

In GraphQL you get exactly what you query for. Each query responds with data and only the data requested, no more no less.

You can find a more comprehensive GraphQL vs REST explanation here.

Parts of the API

I’ll make it a point to group related functionality into apps. This will greatly help in structuring the API and achieve some degree of loose coupling, with unrelated aspects of the API interacting only when necessary. I might need to add more, but the beginner candidates include:

  • Authentication App
  • Friends App
  • Profile App

Project Set-Up

You can start off by creating a project folder by running the command mkdir GraphQLSocial in the terminal from your home folder preferably,

I’ll be using a unix environment, this means for users of the windows OS you might need to check the windows shell equivalent of the commands i’ll be running. Alternatively you might consider developing on the Windows Subsystem for Linux or a unix based terminal.

The command creates a folder where all your code for the project will live. You’ll be using Django to develop the API, it’ll help offload some effort, by for example providing an inbuilt ORM for you to interact with the DB more easily.

Some other packages that’ll be essential in development include psycopg2, a python wrapper for Postgres as well as django-graphene a GraphQL wrapper made specifically for Django it is based on the python graphene package.

Let’s begin by installing the requirements. From the root directory (created earlier) run the below commands in sequence.

touch .python-version  &&  echo "graphqlsocialenv" >> .python-version
pyenv virtualenv 3.8.6 graphqlsocialenv
pip install django psycopg2 graphene-django

Firstly, you create a .python-version file that’ll be referenced every time you cd into the folder and will automatically activate the python environment. The second command creates and automatically activates the python environment, the final line installs all our dependencies.

As a side note if not already you should install a PostgreSQL instance on your machine from here.I’ll be using it as the Database of choice. You can also get this extension if you’ll be using VS Code as the editor of choice.

Once you have all the dependencies installed you can proceed to create the Django project. You’ll do so by running the command

django-admin startproject social . 

The command creates a project that comes with a bunch of files that’ll be useful as we proceed (note the period after social) your folder structure should resemble:

.
├── manage.py
└── social
    ├── __init__.py
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

Next, you’ll create the first app by running from the root folder the command:

django-admin startapp authentication

The command will create an app folder named authentication with a migrations folder that’ll keep track of your DB fields changes. You’ll need to add the app to the INSTALLED_APPS list in your settings.py file. You’ll also need to add graphene_django app in the INSTALLED_APPS list as well. The INSTALLED_APPS list should resemble:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'graphene_django',
    'authentication'
]

Your folder structure should now resemble:

.
├── authentication
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   └── views.py
├── manage.py
└── social
    ├── __init__.py
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

Moving forward, that’s the pattern you’ll be following when creating a new app.

Having completed the initial setup,you should proceed to run the server and see what you have so far, below is the command

python manage.py runserver

You should receive a warning that you have unapplied migrations, let’s clear out that error by applying the migrations. Run:

python manage.py migrate

Running the server again the error disappears. Navigating to the address http://127.0.0.1:8000/ you should be met with the Django default page. This is an indication that the app is set up correctly.

Next, you’ll create a model for the User, instead of creating everything from scratch you’ll be borrowing a few ‘parts’ from the default Django auth model. You’ll do this by creating your own model and inheriting from the Django BaseUserManager. Add the below code to your authentication/models.py file.

from django.contrib.auth.base_user import AbstractBaseUser
from django.core.exceptions import ValidationError
from django.db import models
from django.contrib.auth.models import BaseUserManager, PermissionsMixin

Create your models here.

class UserManager(BaseUserManager):
    """
    Django requires that custom users define their own Manager class. By
    inheriting from `BaseUserManager`, we get a lot of the same code used by
    Django to create a `User` for free.
    All we have to do is override the `create_user` function which we will use
    to create `User` objects.
    """

    def create_user(self, username, email, password=None):
        if email is None:
            raise ValidationError("Users must have and email.")

        user = self.model(username=username, email=self.normalize_email(email))
        user.set_passsword(password)
        user.save()
        return user

class User(AbstractBaseUser, PermissionsMixin):
    username = models.CharField(max_length=255, null=True)

    # We want to index this column in the database to improve lookup performance.
    email = models.EmailField(db_index=True, unique=True, null=False)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)

    USERNAME_FIELD = 'email' # indicates to django to use this as the login field instead of the username

    # Tells Django that the UserManager class defined above should manage
    # objects of this type.
    objects = UserManager()

    def __str__(self):
        return self.email
    

The comments in the code should explain much of what is going on, you are using the already existing auth model/structure and overriding aspects you prefer customized to your own needs. Note, you’ll be using the email and not the username for authentication purposes.

You’ll need to inform Django of the new custom auth model, by adding this line to the settings.py file.

AUTH_USER_MODEL = 'authentication.User'

You can run the migrations again to update the DB. Speaking of which we forgot to plugin the Postgres information, let’s do that now. In your settings file replace the DATABASES field with this.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': os.getenv('DB_NAME', 'postgres'),
        'USER': os.getenv('DB_USER', 'postgres'),
        'PASSWORD': os.getenv('DB_PASSWORD', 'postgres'),
        'HOST': os.getenv('HOST', 'localhost'),
        'PORT': os.getenv('PORT', 5432),
    }
}

The above snippet makes Postgres our default database, you’ll notice I’ve used environment variables for the sensitive parts of the DB information, this comes highly recommended as a security precaution. Remember to import the os module.

Create a .env file at the root of your project and add your env variables there. Mine looks like this:

export DB_NAME='your_db_name'
export DB_USER='your_db_user'
export DB_PASSWORD='your_db_password'

Add the variables by running:

source .env

Now you may proceed to run the migrations, by executing these commands sequentially:

python manage.py makemigrations
python manage.py migrate

The commands will create a new file in authentication/migrations folder, tracking the new changes made. Now that you have the model part finalized you’ll proceed to work on the schemas and mutations.

We’ll be using JsonWebTokens as our authorization mechanism, in that regard we’ll need a package that’ll make our life easier, install the package by running:

pip install django-graphql-jwt

You’ll also need to add a few more configurations in the settings file:

GRAPHENE = {
    'SCHEMA': 'social.schema.schema',
    'MIDDLEWARE': [
        'graphql_jwt.middleware.JSONWebTokenMiddleware',
    ],
}
AUTHENTICATION_BACKENDS = [
    'graphql_jwt.backends.JSONWebTokenBackend',
    'django.contrib.auth.backends.ModelBackend',
]

Schemas, Queries and Mutations

In your social folder you’ll add a schema.py file and in it add below code:

import graphene
from authentication.schemas import AuthMutation, AuthQuery

class Query(AuthQuery, graphene.ObjectType):
    pass

class Mutation(AuthMutation, graphene.ObjectType):
    pass

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

Don’t worry about the errors as a result of the inexistent files, you’ll create them next. In your authentication app folder create a new python package and label it schemas, it’ll hold our mutation and query files. Inside the package create two new files, auth\_queries.py and auth\_mutations.py. Your folder structure so far should resemble:

.
├── authentication
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   ├── __init__.py
│   │   └── __pycache__
│   ├── models.py
│   ├── schemas
│   │   ├── __init__.py
│   │   ├── auth_mutations.py
│   │   └── auth_queries.py
│   └── views.py
├── manage.py
└── social
    ├── __init__.py
    ├── asgi.py
    ├── schema.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

User Registration Mutation

You’ll begin with the auth_mutations file, mutations in GraphQL alter the data, they handle the CUD part of the CRUD abbreviation, the Create, Update and Delete capabilities of the API. The auth_mutations file should have the following code:

import graphene
from django.db import transaction
from graphene_django import DjangoObjectType

from authentication.models import User

class UserType(DjangoObjectType):
    class Meta:
        model = User

class RegisterUser(graphene.Mutation):
    user = graphene.Field(UserType)

    class Arguments:
        username = graphene.String()
        email = graphene.String()
        password = graphene.String()

    errors = graphene.String()
    success_message = graphene.String()
    token = graphene.String()

    
    def mutate(self, info, **kwargs):
        username = kwargs.get('username')
        email = kwargs.get('email')
        password = kwargs.get('password')

        user_data = dict(username=username, email=email, password=password)
        message = "User created {} successfully.".format(email)

        with transaction.atomic():
            user = User(**user_data)
            user.set_password(kwargs.get('password'))
            user.save()
            return RegisterUser(user=user, success_message=message)

class Mutation(graphene.ObjectType):
    register_user = RegisterUser.Field()

You’ll start off with the User registration/signup mutation. The UserType class is a django-graphene helper that transforms our User model into an object type. You’ll then proceed to write a RegisterUser class which will inherit from graphene.Mutation class, you’ll override the mutate class and this is where you’ll add your custom code to add a new user into the DB.

You’ll fetch all the fields supplied by the user which will be passed as keyword arguments in the mutate class. The class Arguments defines the expected data types from our user, in our case all are Strings. We wrap the save sequence in an atomic block to mitigate against data corruption (we roll back all changes in case something goes wrong).

You’ll proceed to add the below code to the __init__.py file in the schemas folder.

from .auth_mutations import Mutation as AuthMutation

In the schema.py file remove any mention of Query and remain only with the Mutation. That should remove the errors that were there earlier.

Finally, you’ll need to edit the urls.py file, with a single endpoint for your GraphQL API, that’s right a single endpoint to rule them all. Add the code below to your urls.py:

.....
#intial code remains
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView

urlpatterns = [
    ...
    path('social/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]

The First User

Visiting http://127.0.0.1:8000/social/ should present you with a GraphQL page from where you can run all manner of commands, it should resemble:

We can proceed to register a new user by typing the below command on the left top side of the screen, feel free to delete the comments.

mutation RegisterUser($email: String!, $username: String!, $password: String) {
  registerUser(email: $email, username: $username, password: $password){
    user {
      email
    }
    successMessage
    errors
  }
}

The statements preceded by $ are variables and you’ll need to provide the values, you should see a section on the bottom left labelled QUERY VARIABLES click on it and add below section:

{
  "email": "email@email.com",
  "username": "username",
  "password": "pass1234"
}

The above should be indicative of credentials a user would plug-in during normal registration. Finally, click on the play button above and you should see a success/failure message on the right side of the screen. That should do it for the registration mutation.

Fetching Users

Let’s tackle the Query, in the auth_queries.py file add:

import graphene
from graphene_django import DjangoObjectType
from graphql import GraphQLError
from ..models import User

class QueryUserType(DjangoObjectType):
    class Meta:
        model = User
        fields = "__all__"

class Query(graphene.ObjectType):
    user = graphene.Field(QueryUserType, id=graphene.Int(required=True))
    users = graphene.List(QueryUserType)

    def resolve_users(self, info):
        return User.objects.all()

    def resolve_user(self, info, **kwargs):
        id = kwargs.get('id')
        try:
            return User.objects.get(pk=id)
        except:
            raise GraphQLError("User does not exist")

In the schemas/__init__.py file add;

from .auth_queries import Query as AuthQuery

Finally, your schema.py file should look like this:

import graphene
from authentication.schemas import AuthMutation, AuthQuery

class Query(AuthQuery, graphene.ObjectType):
    pass

class Mutation(AuthMutation, graphene.ObjectType):
    pass

schema = graphene.Schema(query=AuthQuery, mutation=Mutation)

You can test out the Query by running the Query below from the someplace you did the mutation:

{
  users {
    username
    email
  }
}

You can add more fields in the users’ brackets to get a larger return object e.g. id. To fetch a single user we alter the query to resemble:

{
  user(id: 1) {
    username
    email
  }
}

The Login Flow

Now that you have the registration working we can start working on the login process, which will be our last part in the authentication flow. Let’s head back to the auth_miutations file and make a couple of edits to the file.

You’ll start by adding a new LoginMutation class that will hold your login logic. Add the below code into the file:

# registration code remains
class LoginMutation(graphene.Mutation):
    user = graphene.Field(UserType)
    errors = graphene.String()
    token = graphene.String()
    success = graphene.String()

    class Arguments:
        email = graphene.String()
        password = graphene.String()

    def mutate(self, info, **kwargs):
        email = kwargs.get("email")
        password = kwargs.get("password")
        user = authenticate(username=email, password=password)

        error_message = "Invalid login credentials"
        success_message = "Logged in!"

        if user:
            token = jwt_encode(jwt_payload(user))
            return LoginMutation(user=user, token=token, success=success_message)
        else:
            return LoginMutation(errors=error_message)

class Mutation(graphene.ObjectType):
    register_user = RegisterUser.Field()
    login_user = LoginMutation.Field()

Most of the code in this mutation mirrors the one in the RegisterUser mutation and we’ll not go through it again. Noteworthy is the authenticate function supplied to us by Django, it verifies that the user attempting to login actually exists in the system and the credentials are valid.

We check for a user or None return type, if the former we use the returned user object to generate a JSON web token for authorization purposes moving forward. If the latter we return an error message communicating invalid credentials. We can test this out.

Head to the GraphQL UI and just as we did the registration process add the required variables below while adding the mutation on the left side, my mutation resembles:

mutation loginUser($email: String!, $password: String!) {
  loginUser(email: $email, password: $password) {
    user {
      email
      username
    }
    token
    success
    errors
  }
}

And the variables:

{
"email": "email@email.com",
"password": "pass1234"
}

If the credentials checkout, you should receive a response with a login successful message on top of the JsonWebToken for authorization.

This concludes the first part of this series and we’ll start on the Profile App the next time.

The code so far can be found here.

Check out my other works here