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.
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
.
Next, fill in the database name, then switch to the security tab, and grant all privileges to postgres
user.
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 requireddescription
- A brief description of the nature of the taskis_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.
Method | Use 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!