[Overview] Building a Full Stack Quiz App With Django and React

Izen Oku
The Startup
Published in
13 min readSep 12, 2020
Screenshot of quiz web application

My main goal with this application was to develop a remotely hosted quiz application, which users can choose a quiz, answer the questions, and know how he/she did at the end. This article focuses on: 1. building a Django backend (database & API) 2. hosting the backend and 3. hosting the frontend from a high-level understanding. If you wish for more detailed follow-alongs or tutorials, I would recommend checking out other sources.

Links to the live servers and my GitHub repository can be found at the end of this article.

Table of Content:
1. Building the Django backend
2. Building the React frontend
3. Hosting the Backend (with Heroku)
4. Hosting the Frontend (with Netlify)
5. Lessons Learned & Links

Building the Django Backend

I originally tried following William Vincent’s “Django Rest Framework with React Tutorial” (which is a great tutorial), but knowing how I learn, I knew I needed an understanding from the ground up, as opposed to a quick follow-along.

To understand Django at a deeper level, I subscribed to Treehouse’s “Exploring Django” course, which gave me a decent understanding of how Python’s Object-relational Mappers work, and how Django’s REST framework could be used to configure APIs. If you wish to learn Django from the ground up, I would recommend you to find an online course as well.

The Big Picture & End Goal

This section of the article tackles 2 main questions: 1. how to define the database schema, and 2. how to configure request & response (essentially the API) using Django’s REST framework.

For this project, I knew I wanted to make users, and under each user there could be multiple quizzes, and under each quiz could be multiple questions, and under each question would be multiple answers.

Backend models diagram. “Foreign Key” refers to keys in a database object that points to a foreign table (e.g. each question object has a quiz key that points to a specific quiz object).

Furthermore, coming from a frontend background, I knew I needed to configure urls to fetch: 1. available quizzes for the user to choose from 2. specific questions under chosen quiz and its answer options, and potentially 3. the results on how the user did.

Defining the Database (Models)

Once I created the Django project (I named it ‘quiz_api’), and then the Django app within the project (‘quizzes’), I configured quizzes/models.py to reflect the model diagram. Below is an example of the ‘Quiz’ model object:

from django.db import models
from django.contrib.auth.models import User
class Quiz(models.Model):
author = models.ForeignKey(User, on_delete=models.DO_NOTHING)
title = models.CharField(max_length=255, default='')
created_at = models.DateTimeField(auto_now_add=True)
times_taken = models.IntegerField(default=0, editable=False)
@property
def question_count(self):
''' Method to get num of Qs for this quiz, used in Serializer'''
return self.questions.count()

class Meta:
verbose_name_plural = "Quizzes"
ordering = ['id']
def __str__(self):
return self.title

Once the models were defined, the next step for me was to figure out how to take a client request, process it, and return a response back to the client.

Using Django REST Framework to Configure Reponses (the API)

What is Django REST Framework (DRF)? DRF can be thought of as a library that you can install into your Django project. We can import useful classes from DRF such as viewsets, serializers, and routers. Using these classes can save you time and reduce the code size.

To understand how DRF fits into the picture, first I’ll explain the general request & response pattern between client & server. Below is a brief outline:

Brief outline of how client & server communication works.

As we can see from the diagram, the logic of the server can be broken down into 3 parts: 1. recieve requests and look for a matching url pattern 2. map the request to a corresponding function (to process & configure the response), and 3. return the reponse. This is where DRF’s routers (url patterns), view classes, and serializers come into play.

So where does DRF come into play? Specifically, how does DRF’s views, serializers, and routers help the logic of the server? Routers map URL patterns to specific view functions, view functions extract the correct data and calls the serializer to turn the data into readable JSON format. Below I’ve made a brief flow chart:

Flow chart of how the server handles requests.

Now that I’ve explained how DRF’s viewsets, serializers, and routers are connected, lets dive into the details and see them in action!

Firstly, a Disclaimer: DRF’s Generic Views v.s. ViewSets

I have to mention that in my project, I have 2 sets of APIs. This is because I was following along Treehouse’s tutorial and exercised 2 different methods of defining view classes.

The top 6 functions are defined via the first method, the bottom 3 via the second. Both methods have the same list & retrieve functionalities but method 2 has less lines of code.

In the first method, I created functions ListCreate<ModelName> & RetrieveUpdateDestroy<ModelName> for each model (quiz, question, answer). This set of view functions extends the DRF generics’ subclasses ListCreateAPIView, and RetrieveUpdateDestroyAPIView, respectively. As suggested by the class names, the ListCreate function handles listing and creating the specified model, wehreas RetrieveUpdateDestroy handles retrieving, updating, and destroying the models. The url patterns for this set of view functions are defined in quizzes/urls.py.

In the second method, I created functions <ModelName>ViewSet for each model. This set of functions extends DRF’s viewsets subclass ModelViewSet. The great thing about using viewsets is that 1. ModelViewSet class already covers the basic create, read, update, delete (CRUD) operations (explained in detail below), and 2. there is no need to configure a urls.py file in the app folder. Instead, we import DRF’s routers class in the root folder’s urls.py, and the URLs are already configured for you.

For the rest of the article, I will be referring to the URLs and view functions defined in method 2 (DRF’s viewsets). Having clarified this, let’s first take a closer look at writing the view functions.

View Functions

By inheriting the properties of ModelViewSets, writing these view functions were actually quite straightforward. All I had to do was specify the queryset this particular view function will use , and the corresponding serializer class. I’ll get to my serializers in a second. My Quiz view function is show below:

class QuizViewSet(viewsets.ModelViewSet):
queryset = models.Quiz.objects.all()
serializer_class = serializers.QuizSerializer
@action(detail=True,methods=['get'])
def questions(self, request, pk=None):
questions = models.Question.objects.filter(quiz_id=pk)
serializer = serializers.QuestionSerializer(
questions,
many=True
)
return Response(serializer.data)
@action(detail=True,methods=['get'])
def all_questions(self, request, pk=None):
questions = models.Question.objects.filter(quiz_id=pk)
serializer = serializers.QuestionSerializer(
questions,
many=True
)
return Response(serializer.data)

I can define ad-hoc actions with the “@action” decorator. As the name suggests, by setting these ad-hoc actions, I can define more specific URL patterns than the standard methods that ModelViewSet provides.

In this specific scenario, I’ve added the extra actions ‘questions’, and ‘all_questions’ to the QuizViewSet class. ‘questions’ will return a paginated list of questions under the specified quiz. ‘all_questions’ will return all all questions under the specified quiz. The reason why I’ve added these ad-hoc actions is because I needed responses more specific than the stadard list of questions. Later, I will explain how to get the list of quizzes and how to use the ad-hoc actions in the routers section below.

Serializers

We saw in the previous picture that in the ModelViewSet class, we have to define a serializer class. So what do serializers do? The serializer class dictates how the recieved queryset will be shaped into a JSON object for the reponse. For example, I defined my Quiz serializer as such:

class QuizSerializer(serializers.ModelSerializer):
def getFullname(self, obj): ...
def getQCount(self, obj):
return obj.question_count #we defined this method in models.py
questions=serializers.HyperlinkedRelatedField(...)
author_fullname=serializers.SerializerMethodField("getFullname")
question_count=serializers.SerializerMethodField("getQCount")
class Meta:
fields = [
'id',
'title',
'author',
'author_fullname',
'question_count',
'created_at',
'questions'
]
model = models.Quiz

The most important part to focus on is in the Meta class. Here, we define the fields and related model. Fields is an array specifying which attributes of the specified model you want to include in the reponse.

In the quiz serializer example, most of the attributes are already defined when we wrote the models in quizzes/models.py earlier. The fields questions, author_fullname, and question_count are custom fields defined above the Meta class, and are attributes that are not defined in the quizzes.models.Quiz model.

Earlier in the QuizViewSet view function definition, we defined the seralizer class as QuizSerializer. This means whenever the response data is returned, QuizSerializer is called. Soon, we will see what this means.

Routers

As mentioned earlier, by using DRF’s viewsets.ModelViewSet in combination with routers, the URLs are already configured for us.

To add matching URLs to the ViewSets functions we defined in quizzes/views.py, we simply import routers from rest_framework, and instantiate a SimpleRouter class. Then, we register the viewsets along with its desired regex pattern (e.g. the URL ‘api/v2/quizzes’ will call views.QuizViewSet).

from django.contrib import admin
from django.conf.urls import url, include
from rest_framework import routers
from quizzes import viewsrouter = routers.SimpleRouter()
router.register(r'quizzes', views.QuizViewSet)
router.register(r'questions', views.QuestionViewSet)
router.register(r'answers', views.AnswerViewSet)
urlpatterns = [...]urlpatterns += url(r'^api/v2/', include((router.urls, 'quizzes'), namespace='apiv2'))

We include the router in the project’s URL patterns by adding an url method to the urlpatterns array (last line in the diagram). A note to add here is that by using method 2’s view sets and routers, we can bypass having to define URLs in the app-specific urls.py file (e.g. ‘quizzes/urls.py’).

Having registered the viewsets’ URLs to the router, and adding router.urls to the project’s URLs, let’s see it in action. Let’s try fetching the ‘api/v2/’ URL:

Response from ‘localhost:8000/api/v2/’

We recieve a “Page not found 404 error” because there is no URL configured as ‘localhost:8000/api/v2/’ followed by nothing. Asides from the top 3 URL patterns,we see a bunch of URLs already defined for us by DRF.

We registered the QuizViewSet with a ‘quizzes’ regex pattern to the SimpleRouter, and in line 4 there’s an URL pattern quizzes/; as the name ‘quiz-list’ suggests, it lists the all the available quizzes. In line 5, we have an URL pattern quizzes/(?P<pk>)/, which asks for a primary key; this retrieves the quiz associated with that primary key.

If we had gone with method 1 of using DRF’s generics classes, we would have had to define a separate set of URLs for the “list all quizzes” and “retrieve a specific quiz” in quizzes/urls.py.

Remeber the ad-hoc actions ‘questions’ and ‘all_questions’ that we defined under QuizViewSet earlier? DRF’s SimpleRouter automatically registered those actions as a valid URL match in lines 6 & 7. If we specify a primary key for a quiz with URL pattern 6 (quiz-all-questions), we will recieve the list of all related questions to that specific quiz. This is not a default URL that comes with ModelViewSet, but rather an user-defined action that DRF automatically added to the list of valid URLs for the request to be matched with.

Wrapping it All Together

Routers define which URLs the request could be matched to. This takes care of the first layer of server logic. If a matching URL is found, a corresponding view function is called. In our case, we used inherited viewsets.ModelViewSet so all we had to do was define the queryset and serializer class. The serlializer defines the JSON object that will be included in the response body.

I promsed to show the seralizer in action earlier. To see it in effect, lets call a valid URL (number 5 ‘quiz-detail’), and see what we get:

A side-by-side comparison of the QuizSerializer’s Meta fields and the first quiz in the reponse body of ‘api/v2/quizzes/’

As we can see, the fields that we specified in QuizSerializer define the JSON object returned in the response.

So there we have it — routers, view functions, and serializers work together to process the client request, pull the necessary data from the database model, and return the configured data as the response.

Building the React Frontend

Having had previous experience building React projects, I already knew how to structure the frontend. For a quick guide on how to start your first React application, you can follow this tutorial.

There were some parts I was uncertain about though, specifically having to do with fetching the data from my backend, and dealing with the asynchronous nature of HTTP request/response. For this project, I wanted to learn: 1. how to fetch data from 3rd party API 2. how to setup asynchronous fetching functions and 3. allowing cross-origin-resource-sharing (CORS).

Fetching Responses

Since I was only building a small-scale application, I decided to centralize my fetch methods in App.js. Specifically, in the componentDidMount and componentDidUpdate functions. In retrospect, I would put the fetch functions in the componentDidMount() or componentWillMount() functions of whichever component the response data should reside in.

Due to the asynchronous nature of HTTP requests/responses, you should wrap whatever function that is fetching with an async declaration, and the await operator whenever you’re dealing with a promise object. To learn more about dealing with asynchronous functions, click on this link.

Here is an example of a fetch function, fetchQuizzes(), which fetches the list of available quizzes in the database:

async fetchQuizzes() {
const { url_header } = this.state
// when running backend locally, 'url_header' might be
// localhost:8000
try {
const res = await fetch(`${url_header}/quizzes/`)
const quizzes_body = await res.json();
if (quizzes_body !== undefined) {
this.setState({
isFetchingQuizzes: false,
quizzes: quizzes_body,
})
}
} catch (error) {
console.log(error)
}
}

Once the promise object is returned, we can parse the HTTP response and get the body information with response.json(). Then, I set the response data to a state variable quizzes with this.setState, which is then passed down to lower components.

Allowing CORS

You might’ve ran into a timeout problem when trying to fetch data from your locally hosted backend. A main culprit is because you haven’t allowed CORS. There’s a few ways to circumvent this problem.

One way is to install the Allow CORS: Access-Control-Allow-Origin Google Chrome extenstion. However, this is should be ran only during development, not production (the eventual remotely hosted website).

Another way is to configure the headers of your requests in the frontend to always include “Access-Control-Allow-Origin = ‘*’”. I played around with this solution once I’ve setup the frontend server, but to no avail.

Eventually what worked for me was to allow CORS from the backend. To allow CORS from Django, you need to 1. install django-cors-headers application and 2. configure the CORS whitelist in settings.py.

The instructions for installing the django-cors-headers application, and adding the necessary apps & middlewares, can be found at Python’s Index Package’s django-cors-headers documentation.

To configure the CORS whitelist, navigate to settings.py and add the following configuration:

# CORS
CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = [
'http://localhost:8000',
'http://localhost:3000', # locally hosted frontend
'https://izens-quiz.netlify.app' # remotely hosted frontend
]

You should add whatever websites you want to share your backend data with in CORS_ORIGIN_WHITELIST. By adding ‘localhost:3000’, I can run the frontend locally and still recieve the backend data. Eventually, I’ll want to put the working frontend site to the list as well (‘izens-quiz.netlify.app’)

If you’re already hosting the backend on a remote server, remember to commit the changes, and push to the working backend site (instructions for doing so is detailed below).

Hosting the Backend

Setting Up the Server & Database

Hosting a Django backend on Heroku is relatively simple. You need to set up a virtual environment, initialize a Heroku repository inside that virtual environment, add the Procfile & requirements.txt, and finally push to the Heroku master branch.

Furthermore, you should also switch the database from SQLite3 to PostgreSQL. This is because so far we have been saving our data to a disk-based storage in the local folder SQLite3 that was automatically created during the Django development. However, due to the ephermal nature of Heroku’s container engines, all disk-based storage will be wiped clean at least once every 24 hours. To fix this, we will move the database to a remotely hosted server (e.g. an AWS EC2 instance).

To learn how to do the steps mentioned above, please follow the step-by-step tutorial on this Youtube video.

Adding Static CSS Files to Deployment

Finally, if you want to keep the styling of the Django administrative page or REST framework when ran on Heroku, you need to install the middleware WhiteNoise so that Heroku can host static css files. To do so, start your virtual environment, and follow these steps:

  1. Install WhiteNoise with pip (or pip3)
pip3 install whitenoise

2. Add whitenoise to MIDDLEWARE in project folder’s settings.py

MIDDLEWARE = [
'whitenoise.middleware.WhiteNoiseMiddleware',
...
]

3. Configure static routes in settings.py

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATIC_FILES_DIR = (
os.path.join(BASE_DIR, 'static'),
)

4. Commit the changes and push to Heroku master branch

git add .
git commit -m "feat: whitenoise & static path"
git push Heroku Master

Hosting the Frontend

Deploying to Netlify

I chose to host the frontend with Netlify, which is usually for static websites (i.e. websites that aren’t fetching & posting data to/from a remote server). It seems to work fine with my small-scale application, however that might not be true when the project size becomes bigger.

The process was relatively straightforward. Since my backend & frontend folders resided within an existing GitHub repository, I simply had to connect that repository to a Netlify project and set the build settings.

Build settings for my frontend deployment (using Netlify)

Since my repository contains both the backend and frontend directory, and I’m hosting the frontend on Netlify, I specify the base directory as ‘frontend’. The build command, ‘yarn build’, is a shortcut to call the command ‘react-scripts build’. This command makes a directory ‘build’ within my frontend directory, and correctly bundles React in production for best performance. The created build directory is what Netlify needs for the ‘publish directory’.

For more detailed instructions, you can visit Netlify’s documentation “Get started with Netlify CLI”.

Lessons Learned & Links

This project has taught me alot. I knew how the HTTP request/response pattern works, but I had never put it to code. There’s alot of details that I didn’t discuss in this article, such as writing a custom paginator, that I was not aware of from a high-level understanding. This project has taught me how to address these details.

A huge learning lesson was learning to write the APIs before starting on the frontend. I made the mistake of writing the frontend first with mock data, and that eventually introduced alot of unnecessary JSON parsing and trasnforming into the frontend logic.

You can find the link to my GitHub repository and working sites for this project here. Thank you for reading!

--

--