Build JSON APIs in Django Without DRF or any other library

Gaurav Jain šŸ†
4 min readJan 9, 2024

--

Many students and new Django developers wonder why Django Rest Framework (DRF) is needed alongside Django. Canā€™t we create APIs using Django alone?
While using these libraries in sizable applications simplifies the development, this post explores Djangoā€™s out-of-the-box capabilities to build JSON-based APIs without relying on external libraries such as DRF or Tastypie.

We will cover the following topics:

  1. Returning JSON Response
  2. Serializing queryset/model objects
  3. Pagination
  4. Handling POST/PUT/PATCH Payload
  5. Authentications & Permissions
  6. Class-based views

1. Returning JSON Response

Instead of using HttpResponse directly, make two changes -
1. Convert the Dict object to a JSON object using json.dumps()
2. Explicitly set the content_type to "application/json"

import json
from django.http import HttpResponse

def index(request):
return HttpResponse(
json.dumps({'message': "Hello Gaurav!"}),
content_type="application/json"
)

However, youā€™ll notice the additional code needed for each JSON response. To avoid this Django offers a class named JsonResponse, which streamlines returning JSON responses without the need for explicit JSON conversion and content type setting each time.

from django.http import JsonResponse

def index(request):
return JsonResponse({'message': "Hello Gaurav!"})

We can also verify the response Content-Type using a simple curl.

curl -i http://127.0.0.1:8000/

2. Serializing queryset/model objects

There are many ways to serialize a Django object or a queryset. Some of them are ā€“

  1. Manually serialize the object.
  2. Use the inbuilt Django-provided serializer to serialize the object.
  3. Use 3rd-party libraries such as DRF.

As we aim for a Pure Django approach, we wonā€™t consider the third option.

  1. Manually Serialize the object.
questions = Question.objects.values_list('id', 'question_text', named=True)

data = [{'id': question.id, 'question_text': question.question_text} for question in questions]

Notice that I utilized the named=True parameter to retrieve the queryset data as named tuples, simplifying attribute access. However, for larger tables or applications with intricate table relationships, this approach might become cumbersome and insufficient.

2. Using an inbuilt serializer

from django.core.serializers import serialize  

# Provide the serialize type such as python, json, xml, yaml, etc.
# Here we are using 'python' because JsonResponse will
# automatically convert it to 'json'
serialize('python', Question.objects.all())

The challenge here is that it returns extra fields that we may not want to expose to users. To address this, customize the serialized data by excluding those fields.

Additionally, Modify the behavior of inbuilt Serializers by extending the relevant Serializer classes and creating custom serializers as required.

from django.core.serializers.python import Serializer as PythonSerializer

class Serializer(PythonSerializer):
# override desired methods and add required logic
...

And then register it through the settings as explained here.

SERIALIZATION_MODULES = {
'python': 'path.to.new.python.serializer'
}

Read more about serializers here.

3. Pagination

In production applications, managing pagination is crucial. Django offers a robust pagination feature to address this need.

>>> from django.core.paginator import Paginator
>>>
>>> Question.objects.count()
13
>>> questions_per_page = 5 # Each page would have 5 questions
>>>
>>> paginator = Paginator(Question.objects.all(), questions_per_page)
>>>
>>> paginator.num_pages # Total number of pages given 5 questions in each page
3
>>> page = paginator.page(1) # Accessing page number 1
>>>
>>> page
<Page 1 of 1>
>>>
>>> page.object_list # To get all the objects in page 1
[{....}]

Combining both serializer and pagination functionalities is possible.

>>> serialized_data = serialize('python', page)

Read more about pagination here.

4. Handling POST/PUT/PATCH Payload

To retrieve JSON data from a POST request, access it via the request object and convert it into a Python object.

import json

def post(self, request):
post_body = json.loads(request.body)
question_text = post_body['question_text']
question_id = post_body['id']
...

5. Authentications & Permissions

If your app requires a personalized authentication system, create a custom Authentication backend class and include it in the settings as demonstrated below. E.g. for a custom Token authentication mechanism

from django.contrib.auth.backends import BaseBackend

class TokenBackend(BaseBackend):
def authenticate(self, request, **kwargs):
# Check the Token and return a user.

# Override other methods (e.g. has_perm()) to manage permissions
AUTHENTICATION_BACKENDS = ['path.to.TokenBackend']

Read more about authentication and permissions here.

6. Class-based views

DRF offers some powerful class-based views (CBV)(and viewsets) that are built upon Djangoā€™s built-in CBV. Using plain CBV can be tedious. Django provides some generic CBV to help with that.

Instead of using from django.views import View you can use below -

from django.views.generic import (
DetailView,
ListView,
CreateView,
UpdateView,
DeleteView
)

Read more about CBV here.

Conclusion

In larger applications, opting for DRF is more practical. It streamlines data serialization/deserialization, and abstracts away API boilerplate, enabling rapid API development. With this foundational knowledge, youā€™re geared up to conquer new challenges šŸ˜Ž. If you want me to write about a topic that is not covered here, do let me know in the comments.

For more such articles, follow me at: https://gauravvjn.medium.com.

--

--

Gaurav Jain šŸ†
Gaurav Jain šŸ†

Written by Gaurav Jain šŸ†

Experienced Python dev, building secure & scalable APIs. If you are seeking short term or long term support with your Python project? Leave a private note!

No responses yet