Categories
Django

Building REST API using pure Django

In this post we will learn how APIs are build using pure Django. It will give you a clear view of how API works under the hood.

What is REST API?

API is an application program interface that allows two applications to communicate with each other. APIs simplifies making programs. Ex: access webcam of your computer. 

A REST API is an API that uses HTTP requests to GET, PUT, POST, and DELETE data. So it’s typically a way to access, process, and handle web resources. Web resources can be data, files, images, videos, audio, etc.

The exchange of those data happens through data types mainly JSON. A rest API is accessed through URI or endpoints.

API endpoint layout:
Scheme://<host>:<port>/<path><?query><#fragment>
Ex: http://example.com/api/status/
{
 {
  "user": 1,
  "content": "this is a first content"
 },
}

Overview of Project

We will create an app that returns the status of users in JSON.

For this, we will use class-based views as code related to specific HTTP methods (GETPOST, etc.) can be addressed by separate methods instead of conditional branching.

By the end of this post, we will create two API endpoints:

  1. StatusListAPIView (`/api/status/`)
    StatusListAPIView will return a query-set and post a new status.
  2. StatusDetailAPIView (`/api/status/id/`)
    StatusDetailAPIView will return a single status object, edit the status, and delete the status.

Before beginning the hands-on, I would recommend you to learn how Django works from my previous posts. So now let’s go ahead and jump in.

Setup REST API Using Pure Django

First and foremost create a project (devapi) named as you like, inside a virtual environment (restapi).

  • create an app called status
  • add the app inside INSTALLED_APP of settings.py
  • migrate the builtin tables to database (sqlite)
  • create a superuser to access the admin panel

If you are new to Django check my previous posts Getting Started with Django and Django Admin Panel.

Create a Model

Create a model called Status with user, content, updated, and timestamp fields.

#status/models.py
from django.db import models
from django.conf import settings

class Status(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    content = models.TextField()
    updated = models.DateTimeField(auto_now=True)
    timestamp = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return str(self.content)[:20]

Register model in admin.py to access CRUD functionality from admin panel

#status/admin.py
from django.contrib import admin
from .models import Status 

admin.site.register(Status)

Serialize the Model

Serialization is the process of translating objects into other data types i.e. bytes to transmit data into network.

The result of the query in Django is an object such that we need to translate the object data into JSON.

#status/models.py
import json
from  django.core.serializers import serialize
from django.db import models
from django.conf import settings

#serialize queryset
class StatusQuerySet(models.QuerySet):
    def serialize(self):
        list_value = list(self.values("id","user","content"))
        return json.dumps(list_value)


class StatusManager(models.Manager):
    def get_queryset(self):
        return StatusQuerySet(self.model,using=self._db)

class Status(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    content = models.TextField()
    updated = models.DateTimeField(auto_now=True)
    timestamp = models.DateTimeField(auto_now_add=True)

    objects = StatusManager() #linking StatusManager

    def __str__(self):
        return str(self.content)[:20]

    #serialize individual instance
    def serialize(self):
        data = {
            "id": self.id,
            "user": self.user.id,
            "content": self.content,
        }
        data = json.dumps(data)
        return data

Here, the serialize() inside a model class status returns a serialized data (JSON) of a single instance. And StatusManager class serializes the data using dot value methods to return a query set.

Writing Views

The views in API will return JSON data such that no template is required i.e. HTTP response with content type JSON. We will use class-based views as code related to specific HTTP methods (GETPOST, etc.) can be addressed by separate methods instead of conditional branching.

Create Forms

We need forms to process and validate the data for POST and PUT method. Also CSRF token is required. So create a forms.py as:

#status/forms.py
from django import forms
from .models import Status

class StatusModelForm(forms.ModelForm):
    class Meta:
        model = Status
        fields = [
            'user',
            'content'
        ]

Handle CSRF

To handle CSRF token, we will create a mixin as:

#status/mixins.py
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator

class CSRFExemptMixin(object):
    @method_decorator(csrf_exempt)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)

And for validating JSON data for POST and UPDATE, we will create a utility to check if the data is JSON or not.

#status/utils.py
import json 
 #validdation json for create and update
def is_json(json_data):
    try:
        real_json = json.loads(json_data)
        is_valid  = True
    except ValueError:
        is_valid = False 
    return is_valid

Write Class-Based Views

We will create two end points so two class is required .i.e. DetailAPIView and ListAPIView

create a detail view class as:

#status/views.py
class StatusDetailAPIView(CSRFExemptMixin, View):
    '''
    Retrive, update, delete
    '''
    def get(self, request, id, *args, **kwargs):
        obj = Status.objects.get(id=id)
        json_data = obj.serialize()
        return HttpResponse(json_data, content_type='application/json')

    def post(self, request, *args, **kwargs):
        json_data = json.dumps({"message": "Not allowed please use create end point .i.e./api/status/"})
        return HttpResponse(json_data, content_type='application/json' ,status=403)


    def put(self, request, id, *args, **kwargs):
        valid_json = is_json(request.body)
        if not valid_json:
            error_data = json.dumps({"message":"Invalid data"})
            return HttpResponse(error_data, content_type='application/json', status=400)

        obj = get_object_or_404(Status, id=id)
        data = json.loads(obj.serialize())
        passed_data = json.loads(request.body)
        for key, value in passed_data.items():
            data[key] = value
        form = StatusModelForm(data, instance=obj)
        if form.is_valid():
            obj = form.save(commit=True)
            obj_data = json.dumps(data)
            return HttpResponse(obj_data, content_type='application/json', status=201)
        if form.errors:
            data = json.dumps(form.errors)
            return HttpResponse(data, content_type='application/json', status=400)

        json_data = json.dumps({"message":"sth"})
        return HttpResponse(json_data, content_type='application/json')

    def delete(self, request, id, *args, **kwargs):
        obj = get_object_or_404(Status, id=id)

        deleted_ = obj.delete()
        print(deleted_)
        json_data = json.dumps({"message":"Successfully deleted"})
        return HttpResponse(json_data, content_type='application/json', status=200)

The class StatusDetailAPIView works (/api/status/<id>/) as:

  • inherits view (class-based view) and CSRFExemptMixin.
  • separate HTTP method as
    • GET a single status object and serialize to JSON
    • POST is not applicable in this endpoint
    • PUT/UPDATE an existing status object by validating the input data
    • DELETE an existing status

create a list view class as:

#status/views.py
class StatusListAPIView(CSRFExemptMixin,View):
    '''
    list retrive and create
    '''
    def get(self, request, *args, **kwargs):
        qs  = Status.objects.all()
        json_data = qs.serialize()
        return HttpResponse(json_data, content_type='application/json')

    def post(self, request, *args, **kwargs):
        valid_json = is_json(request.body)
        if not valid_json:
            error_data = json.dumps({"message":"Invalid data"})
            return HttpResponse(error_data, content_type='application/json', status=400)
        data = json.loads(request.body)
        form = StatusModelForm(data)
        if form.is_valid():
            obj = form.save(commit=True)
            obj_data = obj.serialize()
            return HttpResponse(obj_data, content_type='application/json', status=201)
        if form.errors:
            data = json.dumps(form.errors)
            return HttpResponse(data, content_type='application/json', status=400)
        data = json.dumps({"message": "unknown"})
        return HttpResponse(data, content_type='application/json', status=400) #400 bad request

    def delete(self, request, *args, **kwargs):
        data = json.dumps({"message": "you can't delete an entire list"})
        return HttpResponse(data, content_type='application/json', status=404) #404 forbidden

The class StatusListAPIView works (/api/status/) as:

  • inherits view (class-based view) and CSRFExemptMixin.
  • separate HTTP method as
    • GET a query set and serialize to JSON
    • POST a new status by validating the input data
    • DELETE an entire list of data is not applicable

Mapping URLs

Each class in views represents an API endpoint such that there are two api endpoints.

Add a new urls.py file in status application directory

#status/urls.py
from django.urls import path
from status.views import StatusDetailAPIView, StatusListAPIView

urlpatterns = [
    path('', StatusListAPIView.as_view()), #/api/status/ -> list/create
    path('<id>/', StatusDetailAPIView.as_view()), #/api/status/<int>/ ->detail/update/delete
]

Since the views are in class-based view, it is converted into function-based view using as_view().

Also include the app URLs in project URLs

#devapi/urls.py
from django.contrib import admin
from django.urls import path, include

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

Now you can test the API through postman by starting the development server.

Django API returns list of object in JSON

Wrapping up

In this post, we learn to build the REST API using pure Django. The API is not complete for production. To add other functionalities such as authentication, search, etc it will be complex. So in the next post, we will build a fully functioning API using the Django REST framework.