Switching up my stack: Django, MongoDB, GraphQl, React Native.

Greetings developers, COVID-19 has granted many of us with a lot of time on our hands. With this time I have been working on a personal project using my standard weapon of choice Node JS, I, however, have been looking for something that would stretch me out of my comfort zone. So I have decided to look into converting my current stack from Express JS and Nuxt JS to Django and React Native. Within this series of articles, I will show you how I utilised different technologies to mimic my current project.

Image for post
Image for post

Why am I using Django?

My first reason for Django would simply be how rapidly you can your project into production with the ability to still write DRY and scalable code, even self-proclaiming themselves as “The Web Framework for perfectionists with a deadline”. My other reason would be that Django is well maintained and established, this allows for a big community for help, resources, and the bonus of frequent security patches.

Why am I using React Native?

I decided to go with React Native mainly down to the fact that from me to produce a web, desktop, mobile, and tablet application I only need to build it once. React native allows you to publish across multiple platforms with pretty great performing applications.

Let’s get started

Within this series, we are going to build the user authentication of my application. This will allow us to register and log in with an account as well as to authenticate through GraphQl Mutations and JSON web tokens (JWT).

For the first article, I am going to show you to set up your project and learn how to replace the default auth user with your own and set up a unit test for the creation of a user and a superuser.

The first thing you will need to do is make sure you have installed Python, The installation is very easy and it is compatible across all operating systems.

When working with Python it is best practice to work within a virtual environment, this will isolate the packages installed within your project. We need to first create your project then initiate and activate your virtual environment:

$ python -m venv venv
/// Linux, Unix, Mac ext
source ./venv/bin/activate
/// Windows
venv\Scripts\activate

Now that we have our environment up and running we need to install our packages and set up our first application this will be called users. Other than using Django the two main packages we will be using are:

graphene-django: Graphene is a Python library that grants you the ability to build GraphQL APIs. The module provides a simple but extendable API for making developers’ lives easier.

djongo: Djongo is an Object Document Mapper (ODM) for MongoDB that you can connect to djongo.

Okay, let’s finish the set up in your virtual environment run the following:

$ pip install django djongo graphene-django django-graphql-jwt 
$ python manage.py startproject <PROJECT NAME>
$ python manage.py startapp users

We now need to add the users app to the INSTALLED_APPS in our settings.py file. We will also need to configure our database to use Djongo to do this replace the DATABASES variable like bellow.

INSTALLED_APPS = [
...
'users',
]
DATABASES = {
'default': {
'ENGINE': 'djongo',
'NAME': '<DBNAME>',
}
}

Now we are ready to start digging into our application and get the user model ready.

The Custom user model.

In my application, I have chosen to overwrite the default User model with my custom one. I wanted to authenticate the user with an email address rather than a username and I wanted to use my fields rather than ones set in the default. We are also going to be creating a custom Manager for registering and fetching users.

With this project, we are going to do TDD so this means we are going to write our tests first and define our expected responses from our Custom user models Managers. Add the following specs to users/tests.py these tests will fail as we have done nothing, all in good time!

from django.test import TestCase
from django.contrib.auth import get_user_model


class UsersManagersTests(TestCase):
"""
Set the user Model.
"""

User = get_user_model()

def test_create_user(self):
"""
Tests creating a standard user.
Test that the user is created with the set variables.
Test if there is a type error if no params are passed in.
Test if there is a type error if no password is passed in.
Test if there is a value error if email is blank.
Test if there is a value error trying
to create a superuser with normal user method.
"""

email = 'conway@trichome.tech'
password = 'smokedope420'
first_name = 'Conway'
last_name = 'Kush'
occupation = 'Software engineer'
company = 'Trichome Tech'
profile_title = 'We out here building cool shit.'

user = self.User.objects.create_user(
email=email,
first_name=first_name,
last_name=last_name,
occupation=occupation,
company=company,
profile_title=profile_title,
password=password
)
self.assertEqual(user.email, email)
self.assertEqual(user.first_name, first_name)
self.assertEqual(user.last_name, last_name)
self.assertEqual(user.occupation, occupation)
self.assertEqual(user.company, company)
self.assertEqual(user.profile_title, profile_title)
with self.assertRaises(TypeError):
self.User.objects.create_user()
with self.assertRaises(TypeError):
self.User.objects.create_user(email='')
with self.assertRaises(ValueError):
self.User.objects.create_user(
email='',
password=password
)
with self.assertRaises(ValueError):
self.User.objects.create_user(
email='failuser@fail.com',
password=password,
is_superuser=True
)

def test_create_superuser(self):
"""
Tests creating a superuser.
Test that the superuser is created with the set variables.
Test if there is a value error trying
to create a normal user with superuser method.
"""

email = 'k@kidcurrent.tv'
password = 'weouthere420'
first_name = 'Kid'
last_name = 'Current'
occupation = 'Product Designer'
company = 'KidCurrent TV'

user = self.User.objects.create_superuser(
email=email,
first_name=first_name,
last_name=last_name,
occupation=occupation,
company=company,
password=password
)

self.assertEqual(user.email, email)
self.assertEqual(user.first_name, first_name)
self.assertEqual(user.last_name, last_name)
self.assertEqual(user.occupation, occupation)
self.assertEqual(user.company, company)
self.assertTrue(user.is_superuser)

with self.assertRaises(ValueError):
self.User.objects.create_superuser(
email='superfail@fail.com',
password='foo',
is_superuser=False
)

def test_get_by_id(self):
"""
Tests user get by id.
Tests if method has value
Tests if user exists
"""

email = 'k@kidcurrent.tv'
password = 'weouthere420'
first_name = 'Kid'
last_name = 'Current'
occupation = 'Product Designer'
company = 'KidCurrent TV'

user = self.User.objects.create_superuser(
email=email,
first_name=first_name,
last_name=last_name,
occupation=occupation,
company=company,
password=password
)

user = self.User.objects.get_by_id(user.id)

self.assertEqual(user.email, email)
self.assertEqual(user.first_name, first_name)
self.assertEqual(user.last_name, last_name)
self.assertEqual(user.occupation, occupation)
self.assertEqual(user.company, company)
self.assertTrue(user.is_superuser)

with self.assertRaises(ValueError):
self.User.objects.get_by_id()

with self.assertRaises(self.User.DoesNotExist):
self.User.objects.get_by_id(99)

That’s it for our tests this will come in handy later when we are creating our Custom user model.

In Django a Manager is an is class that contains database query operations. These operations can be used to make queries to the database based on your model. We are going to extend the BaseUserManager and add our methods:

from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import ugettext_lazy as _


class CustomUserManager(BaseUserManager):
"""
Custom user model manager.
"""


def create(self, email, password, **fields):
"""
Create and save a User.
"""

email = self.normalize_email(email)
user = self.model(email=email, **fields)
user.set_password(password)
user.save()
return user

def create_user(self, email, password, **fields):
"""
Create and save a Normal User.
"""

if not email:
raise ValueError(_('The Email must be set'))
if fields.get('is_superuser') is True:
raise ValueError(
_('Normal user cannot be a super user.')
)
return self.create(email, password, **fields)

def create_superuser(self, email, password, **fields):
"""
Create and save a Super User.
"""

fields.setdefault('is_superuser', True)

if fields.get('is_superuser') is not True:
raise ValueError(_(
'Super user must have super_user set to True'
))
return self.create(email, password, **fields)

def get_by_id(self, id=None):
"""
Get a user by ID
"""

if not id:
raise ValueError(_('An ID is required'))

return self.get(pk=id)

We now have the create user methods to allow us to create a normal user as well as a superuser and one to fetch a user by ID. Next, we will create our Custom user model.

Finally, we are creating our User Model, when creating the Model you can choose between subclassing AbstractUser or AbstractBaseUser.

AbstractUser contains the existing fields on the User model, you could use this if you just wanted to replace the username field.

AbstractBaseUser would allow you to have a fresh user model so that you can do as you wish! This is the class I will be subclassing as well as the PermissionsMixin which adds the fields and methods that are required to support Group and Permission models.

Thanks to our tests we know exactly what we need to add to our Custom user Model so open up users/models.py and replace everything with this:

from djongo import models
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.utils import timezone

from .managers import CustomUserManager


class CustomUser(AbstractBaseUser, PermissionsMixin):
"""
Custom user model.
Set username field to 'email'.
Set required fields 'first_name'.
Overwrite objects with our custom user manager.
"""

email = models.EmailField(unique=True)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100, blank=True)
occupation = models.CharField(max_length=100, blank=True)
company = models.CharField(max_length=100, blank=True)
profile_title = models.CharField(
max_length=100,
blank=True,
default='What I\'m doing')
profile_description = models.TextField(blank=True)
date_joined = models.DateTimeField(default=timezone.now)

USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['first_name']

objects = CustomUserManager()

def __str__(self):
return self.email

Now we will need to add the following into our settings file to overwrite the default user model.

AUTH_USER_MODEL = 'users.CustomUser'

We can now create our migrations, the best thing to do to make sure you have set everything up correctly is to do a dry run, this will show you what you are applying without writing to your database. Run the following to do the dry run this will show you a generated Migration class with operations that looks like the image below.

$ python manage.py makemigrations --dry-run --verbosity 3
Image for post
Image for post

Now that we know that our migrations are what we wish to migrate to our database, we can wet run our migrations.

$ python manage.py makemigrations
$ python manage.py migrate

This may fail for some people due to an issue with sqlparse.You will need to install version 0.2.4 .

$ pip uninstall sqlparse
$ pip install sqlparse==0.2.4
Image for post
Image for post
Your schema will be created within your database.

Now we can run our tests and see our test cases pass when running our creation methods.

$ python manage.py test
Image for post
Image for post

When our tests have passed we can create a superuser with the following command:

$ python manage.py createsuperuser
Image for post
Image for post

Woohoo! We have a custom user model, its time for us to start digging into Graphene and making our application GraphQL ready!

Image for post
Image for post

Creating the Users GraphQL schema.

We have our user model so we can create connect Graphene to our application and create a Query to fetch a list of users and a Mutation to allow users to create an account. First, we want to add Graphene to our application add the following in the settings.py file within the INSTALLED_APPS array.

INSTALLED_APPS = [
...
'graphene_django',
]

We are going to add two queries for our User model, these will be user to get single-user information and users to get a list of all the users. We will also add filters to the In our user application create a file called schema.py and we will add the User type and queries to fetch one or more user.

from graphene import Mutation, ObjectType, List, Field, Int, String, ID
from graphene_django.types import DjangoObjectType
from .models import CustomUser as User
class UserType(DjangoObjectType):
"""
User model's fields are automatically mapped onto the UserType.
"""
class Meta:
"""
User model is configured here.
Set fields you wish to be outputted.
"""

model = User
fields = (
'id',
'first_name',
'last_name',
'last_login',
'email',
'occupation',
'company',
'profile_title',
'profile_description',
'date_joined',
)
class Query(object):
"""
User queries.
"""

users = List(UserType)
user = Field(UserType, id=Int())
@staticmethod
def resolve_users(self, info, **kwargs):
"""
Resolves all users.
"""

return User.objects.all()
@staticmethod
def resolve_user(self, info, **kwargs):
"""
Resolves a single user by ID.
"""

return User.objects.get_by_id(**kwargs)

Note that the above Query class is a mixin, inheriting from object. This is because we will now create a project-level query class which will combine all our app-level mixins.

Create the parent project-level cookbook/schema.py:

As you can see the Query class is a mixin that inherits object. This is so when we create our projects mainQuery class, we can combine all of our apps Query mixins.

For the projects Query class create the file <PROJECT NAME>/schema.py and add the following:

import grapheneimport users.schema
class Query(users.schema.Query, graphene.ObjectType):
"""
Projects main Query class, this will inherit multiple queries.
"""

pass
schema = graphene.Schema(query=Query)

We now need to attach this schema to our project within your settings.py file at the bottom add:

GRAPHENE = {
'SCHEMA': 'switchingupstack.schema.schema',
}

Finally, we can set up our URLs so that we have an endpoint for GraphQl and access to GraphiQl. Within the file<PROJECT NAME>/urls.py we need to make sure it has the following:

from django.conf.urls import url, include
from django.contrib import admin
from graphene_django.views import GraphQLViewurlpatterns = [
url('admin/', admin.site.urls),
url('graphql/', GraphQLView.as_view(graphiql=True)),
]

Now we can test our queries within GraphiQl so get the sever up and running and hit the URL we just set up. Probably http://127.0.0.1:8000/graphql/

$ python manage.py runserver

In the left-hand column, you will need to add your queries, you can add both of them and once you hit the run button you are given the choice of which one to run. The queries will look like the following:

query users {
users {
id
email
}
}
query user {
user(id: 1) {
id
email
}
}

Queries are done! let’s add one mutation for us to be able to create a user.

Image for post
Image for post

We got our Queries we are going to create a Mutation for use to be able to register a user to the application. Implementing this is pretty much the same as registering a query, in our users/schema.py file add this bellow our Query class.

class CreateUser(Mutation):
"""
Create a user mutation.
Attributes for the class define the mutation response.
"""

id = ID()
email = String()
first_name = String()
last_name = String()
class Arguments:
"""
Input arguments to create a user.
"""

email = String(required=True)
password = String(required=True)
first_name = String(required=True)
last_name = String()
@staticmethod
def mutate(_, info, email, password, first_name, last_name):
"""
Use the create_user method and return the
attributes we specified.
"""

user = User.objects.create_user(email=email,
password=password,
first_name=first_name,
last_name=last_name)
return CreateUser(
id=user.id,
email=user.email,
first_name=user.first_name,
last_name=user.last_name)
class Mutation(ObjectType):
"""
Mutations for Users.
"""

create_user = CreateUser.Field()

And now back in our project schema.py file we can add the main mutation mixin.

class Mutation(users.schema.Mutation, graphene.ObjectType):
"""
Projects main Mutation class, this will
inherit multiple mutations.
"""

pass

Finally change the schema variable to:

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

Boom! Now within GraphiQl, we can create a user!

mutation createUser {
createUser(email: "k@kidcurrent.tv"
password: "420BlazeIt"
firstName: "Kid"
lastName: "Current"){
id
email
firstName
lastName
}
}
Image for post
Image for post

Okay, we got our Queries and our Mutations the final part of this article is the authentication so we can get our users to log-in and use our application!

Django already has authentication and authorisation using sessions out of the box and is enabled by default. We are going to go with a stateless approach and implement a JWT library called django-graphql-jwt.

As the GraphiQl web app is whack and doesn’t allow us to add HTTP headers, we will be required to use another IDE such as GraphQl Playground or Insomnia. Install either of those and let’s get started!

First, we need to configure django-graphql-jwt so in our settings.py file within the MIDDLEWARE array you will need to add:

MIDDLEWARE = [
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
]

Then within the GRAPHENE variable add:

GRAPHENE = {
...
'MIDDLEWARE': ['graphql_jwt.middleware.JSONWebTokenMiddleware']
}

Finally, within this file at the bottom add the following:

AUTHENTICATION_BACKENDS = [
'graphql_jwt.backends.JSONWebTokenBackend',
'django.contrib.auth.backends.ModelBackend',
]

Now we need to add some mutations for us to log the user in and then be able to verify and refresh our token to do this we need to add import graphql_jwt at the top of our users/schema.py file and then replace our Mutation with the following:

class Mutation(ObjectType):
"""
Mutations for Users.
"""
create_user = CreateUser.Field()
login = graphql_jwt.ObtainJSONWebToken.Field()
verify_token = graphql_jwt.Verify.Field()
refresh_token = graphql_jwt.Refresh.Field()

This will add 3 new mutations to our schema. login will be used to authenticate the user with their email and password returning a token, verify_token will confirm the validity of the users token and the refresh_token will generate a new token & expire the old one.

To test the authentication we can create a new query called that will get the user from the context. Within the User queries in the same file add the new queryme = Field(UserType) and then we need to add a new resolver for this query:

@staticmethod
def resolve_me(self, info):
"""
Resolves the logged in user
"""

user = info.context.user
if user.is_anonymous:
raise Exception('You are not logged in')
return user

Now we are going to use the login mutation to get our token, I am using Insomnia.

Image for post
Image for post

We then want to test our me query, to do this add a header called Authorization and add your token with JWT appended. Now fire up that me query!

Image for post
Image for post

And that’s it for authentication, our basic user flow is now complete! We are now able to register a user and log in.

Conclusion

Now that we have this basic user flow we can move onto our React Native application! We will be creating a register and login screen as well as a logged-in user only section. With this knowledge, you will be able to easily extend each part of the tutorial into creating a full-stack Django application.

You can check out the GIT repo for this article and fork it into your project! You can also find me on Instagram and LinkedIn. I know COVID has hit hard for a lot of people, so if you would like some assistance or pick my brain hit me up!

Image for post
Image for post

Written by

Full-stack web & mobile application developer.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store