How to create a RESTful API Service in Django: A Zero Library, Simple Todo API with Django and PostgreSQL

How to create a RESTful API Service in Django: A Zero Library, Simple Todo API with Django and PostgreSQL

Learn how to create an API in Django using zero libraries.

Introduction

Django is a vast framework filled with many already prepared libraries that cover almost every aspect of server-side backend development, but 90% of the time, Django developers spend more time creating SSR (server-side Rendered) websites.

Many Django developers haven't even explored its REST capabilities and often look to other frameworks such as Node JS as a "Go To First" for creating RESTful APIs.

Today we will be looking at how we can create a simple Todo API using pure Django without any libraries such as Django REST Framework

Prerequisites

The tutorial will require that:

  • You have a basic understanding of Django as well as its ORM

  • A basic understanding of how RESTful Services work

  • Latest version of PostgreSQL and pgAdmin4 installed

Getting Started

We will start by creating a new Django Project, and we can do that by simply writing in our terminal.

djanogo-admin startproject todoapi

You should then see a folder in your file explorer called todoapi. Open that folder in the IDE of your choosing. I will be using VSCode for the duration of this article.

Virtual Environment

Next, we have to create a virtual environment for our Django project. We will be using a Python package called virtualenv which we can install by simply writing in our terminal:

pip install virtualenv

After that's done, we now have to create our virtual environment, which we will call venv, and we can do that by simply writing in our terminal:

virtualenv venv

You should now see a folder called venv in your project files window.

Now, we activate our virtual environment.

Linux/Mac

source ./venv/bin/activate

Windows

source ./venv/Scripts/activate

Now that we have activated our virtual environment, we will be installing the latest version of Django and psycopg2-binary (a PostgreSQL database adapter for the Python programming language)

pip install django psycopg2-binary

Next, we will set up our database config in the setting.py located at todiapi/settings.py. Navigate to the DATABASES property which would look something like this:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

Next, replace the DATABASES field with a PostgreSQL database config. First, fill in your database connection credentials. Next, Set the ENGINE property to the python PostgreSQL adapter django.db.backends.postgresql_psycopg2 and, then set the NAME property to todo_api_db or any name of your choice.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'todo_api_db',
        'USER': 'root',
        'PASSWORD': 'password',
        'HOST': 'localhost',
    }
}

Creating the database and running our migrations

Next, we will create our todo_api_db database using pgAdmin4.

Launch pgAdmin4 and enter your master password.

Entering master password into pgAdmin4

Next, select your default/local server and enter your server password.

Next, right click on Databases from the right dropdown menu and select Create > Database.

Creating a database in pgAdmin4

Next, fill in the database name, then switch to the security tab, and grant all privileges to postgres user.

Assigning privileges to postgres user for our database

Next, click on save and continue.

When that's done, we then have to run our migrations:

python manage.py migrate

Now that we have all the necessary dependencies installed, we will create a new Django app next.

Creating The Todo app

Now let's create our todo app.

We will start by creating an app in our Django project, which we will call todo and which we can do by simply writing in our terminal:

python manage.py startapp todo

Next, we have to register our app in the setting.py located at todiapi/settings.py. Navigate to the INSTALLED_APPS list and add the name of our app todo to the bottom of the list. It should now look something like this

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'todo' # <--- add app name here
]

Defining Our Model

Our model will contain the following primary fields:

  • title - The title of our todo item which is required

  • description - A brief description of the nature of the task

  • is_completed - Indicates whether the task is completed or not

Next, we will define our TodoItem model:

from django.db import models
from django.utils import timezone

class TodoItem(models.Model):

    title = models.CharField(max_length=100, null=False, blank=False)
    description = models.TextField(null=False, blank=True, default='')
    is_completed = models.BooleanField(default=False)
    created_at = models.DateTimeField(default=timezone.now)
    completed_at = models.DateField(blank=True, null=True)

    def __str__(self):

        return self.title

Configuring Our Routes

Next, inside our todo app folder, we will create a new file called urls.py and declare our urlpatterns list.

from django.urls import path

urlpatterns = [

]

Next, inside our todoapi folder, open the urls.py file, and add the includes function to the imports.

from django.urls import path, include

and after that, add a path called api/todo/ that includes the urls.py route in our todo app, then specify the relative path to the urls.py file

path('api/todo', include('todo.urls'))

Now, everything should look like this

from django.contrib import admin
from django.urls import path, include

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

Creating Our Endpoints (Views)

Over at our views.py file in our todo folder, we are going to define our endpoints.

In our Todo API, we are going to have five endpoints, namely:

  • POST -> /api/todo/add - adds a single todo item

  • PUT -> /api/todo/update/<id> - updates a todo item by its id

  • DELETE -> /api/todo/delete/<id> - deletes a single todo item by its id

  • GET -> /api/todo/get - gets all todo items

  • GET -> /api/todo/get/<id> - gets a single todo item by its id

  • POST -> /api/todo/complete/<id> - completes a single todo item by its id

Back to our views.py, first, we will import the following.

from django.http.response import HttpResponse
from django.core import serializers
from .models import TodoItem
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.forms.models import model_to_dict
from django.utils import  timezone
import json

and then, we will define our add_todo view:

@csrf_exempt # Tells Django not to worry about cross-site request forgery
# add a todo item to our model
def add_todo(request):

    if request.method == 'POST': # make sure the request is a POST request

        # First we will read the todo data from the request.body object

        body = ''

        try:

            body = json.loads(request.body.decode('utf-8')) # parse JSON body from string to python dictionary

        except json.JSONDecodeError:

            return JsonResponse({
                'status': False,
                'message': 'JSON body was not supplied' # tell the developer to set the content type to application/json and pass and empty json
            })

        title = body.get('title')
        description = body.get('description')

        # next, check if the title is empty or null

        if title is None or title == '':

            # return false message if field `title` is empty or null

            return JsonResponse({
                'status': False,
                'message': 'Field `title` is required' 
            })

        # next, check if the description is empty or null

        if description is None or description == '':

            # return false message if field `description` is empty or null

           description = '' # set description to an empty string

        todo = TodoItem(title=title, description=description) # Initialize a new Todo Item
        todo.save() # save todo item

        #return true message after saving the todo item

        return JsonResponse({
            'status': True,
            'message': f'Todo Item: {title}  has been created'
        })


    # return false message if any other method is used

    return JsonResponse({
        'status': False,
        'message': 'ONLY POST METHOD ALLOWED'
    })

If you noticed, instead of the traditional render() function we would normally return from a view, here we are using a specialized return type; JsonResponse.

JsonResponse behind the hood converts your dictionaries to JSON objects by setting the Content-Type header in the response body to application/json.

Now over to our urls.py file in our todo app, we will import the view we created and include it in our urls.

from django.urls import path
from todo import views

urlpatterns = [
    path('add-todo', views.add_todo, name='add_todo')
]

Perfect!

Now we will add the rest of our routes to our urls.py file.

from django.urls import path
from todo import views

urlpatterns = [
    path('add-todo', views.add_todo, name='add_todo'),
    path('get-todos', views.get_todos, name='get_todos'),
    path('get-todo/<id:int>', views.get_todo_by_id, name='get_todo_by_id'),
    path('update-todo/<id:int>', views.update_todo, name='update_todo'),
    path('complete-todo/<id:int>', views.complete_todo, name='complete_todo'),
    path('delete-todo/<id:int>', views.delete_todo, name='delete_todo')
]

Next, we'll define the rest of our endpoints.

I have included comments on crucial parts of the code to explain what they do.

Getting All Todos

@csrf_exempt # Tells Django not to worry about cross-site request forgery  
# a view to fetch all todo items
def get_todos(request):

    if request.method == 'GET': # make sure the request is a GET request

        todos = TodoItem.objects.values() # query all todo items in our TodoItem model, we use the .values() because Queryset is not serializable
        todos_list = list(todos) # list converts query sets to python readable list

        return JsonResponse({
            'status': True,
            'payload': todos_list # return a field `payload` containing an array of todo items
        }, safe=False) # 

    # return false message if any other method is used

    return JsonResponse({
        'status': False,
        'message': 'ONLY GET METHOD ALLOWED'
    })

Getting A Single Todo

@csrf_exempt # Tells Django not to worry about cross-site request forgery   
# get a single todo item
def get_todo_by_id(request, id): # id of todo item (read from route)

    if request.method == 'GET': # make sure the request is a GET request

        todo_item = TodoItem.objects.filter(pk=id)
        todo_item_exist = todo_item.exists()

        if not todo_item_exist:

            return JsonResponse({
                'status': False, # we return false to tell the frontend that something went wrong
                'message': 'Todo item does not exists' # return a field `payload` containing an array of todo items
            })

        return JsonResponse({
            'status': True,
            'payload': model_to_dict(todo_item.first()) # fetch the first and only item in the Queryset
        })

    # return false message if any other method is used
    return JsonResponse({
        'status': False,
        'message': 'ONLY GET METHOD ALLOWED'
    })

Updating A Single Todo

@csrf_exempt # Tells Django not to worry about cross-site request forgery    
# update single todo item
def update_todo(request, id): # id of todo item (read from route)

    if request.method == 'PUT': # make sure the request is a PUT request

        # First, we will read the todo data from the request.body object

        body = json.loads(request.body.decode('utf-8')) # parse json body from string to python dictionary

        title = body.get('title')
        description = body.get('description')

        # next, check if the title is empty or null

        if title is None or title == '':

            # return false message if field `title` is empty or null

            return JsonResponse({
                'status': False,
                'message': 'Field `title` is required' 
            })

        # next, check if the description is empty or null

        if description is None or description == '':

            # return false message if field `description` is empty or null

            description = '' # set description to an empty string

        todo_item = TodoItem.objects.filter(pk=id)
        todo_item_exist = todo_item.exists()

        if not todo_item_exist:

            return JsonResponse({
                'status': False, # we return false to tell the frontend that something went wrong
                'message': 'Todo item does not exists' # return a field `payload` containing an array of todo items
            })

        todo_item.update(title=title, description=description) # query to update the title and description field

        return JsonResponse({
            'status': True,
            'payload': model_to_dict(todo_item.first()) # fetch the first and only item in the queryset
        })


    # return false message if any other method is used
    return JsonResponse({
        'status': False,
        'message': 'ONLY PUT METHOD ALLOWED'
    })

Great!

We've created most of our endpoints, but here's the caveat! I'm gonna let you create the others yourself :D But don't worry, here's a link to the GitHub repo if you ever get lost.

JSON Serialization

JSON Serialization is the process of encoding objects as strings. In Python, The object in question must be of type dictionary or list to be successfully serialized into JSON.

In Django, if we attempt to serialize the data from our model, it will crash our app. This is because the data returned to us from our model is of type QuerySet.

A QuerySet in Django is a collection of table rows from a table in our database. Our JsonReponse method, which is a type of JSON serializer, does not understand how to handle a QuerySet. To aid our JsonResponse method to serialize our QuerySet, we have converted our QuerySet object to a primitive JSON compactable datatype. These compactable datatypes are list and dictionary.

For example, in our get_todos view, we used the list() method to convert a QuerySet to a list.

todos_list = list(todos) # list converts query sets to python readable list

We used the model_to_dict() method to convert a single row returned from our model or QuerySet into a Python-readable object.

model_to_dict(todo_item.first())

To summarize, when building an API in Django without using any libraries, list() and model_to_dict() methods are our best friends. They are essential because they help us convert our model data to Python-readable objects, which we can then return as JSON to a frontend.

MethodUse case
list()Converts a QuerySet to an array
model_to_dict()Converts a single model row to an object

Conclusion

Thanks for reading! I hope this article helps you understand how to create a RESTFul API service in Django with zero external libraries and simple serialization techniques.

Here's a link to a Postman collection of our API: Django API Collection

Link to the GitHub Repo: Django API Repo

Cheers!