RESTful APIs with Django and the Django Rest Framework

Part 3 - Using Model Serializers with Django Rest Framework

In Django, creating and managing web APIs can be accomplished without any additional tools, but this often involves a lot of repetitive and error-prone manual work. In the last part of this tutorial, we saw how this could be done.

One of the most common tasks in API development is serializing data—converting complex data types like Django model instances into native Python data types that can easily be rendered into JSON or XML. When doing this manually, every field needs to be converted individually, and any custom properties or methods, such as those created with the @property decorator, need to be handled explicitly.

This is where Django Rest Framework (DRF) comes in. DRF provides a powerful and flexible toolkit for building Web APIs, and one of its most useful features is its serializers. DRF serializers take care of the complexities involved in converting model instances to native data types and vice versa. This not only reduces the amount of code you need to write but also minimises the risk of errors. Serializers handle the inclusion of all necessary fields, including custom properties, and ensure data validation and representation consistency.


To build a serializers.py file for our Course model using Django Rest Framework (DRF), we'll need to create a serializer class that translates the Course model instances into JSON-compatible data formats. We also need to ensure that our custom properties like discount_price are included in the serialized output.

In part 1 of this tutorial we saw how to pip install DRF and add it to our list of installed apps in the settings.py file. Activate your virtual environment and run a pip freeze to check you have DRF installed. If not make sure you install it if you haven't already:

pip install djangorestframework

and add rest_framework to your INSTALLED_APPS in settings.py.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Third-party Apps:
    'rest_framework',
    # Custom Apps:
    'api',
    'courses',
]

Next, we create the serializers.py file in our courses app and define the CourseSerializer class in our serializers.py file:

# courses/serializers.py
from rest_framework import serializers
from .models import Course

class CourseSerializer(serializers.ModelSerializer):
    discount_price = serializers.ReadOnlyField()

    class Meta:
        model = Course
        fields = [
            'id',
            'title',
            'course_id',
            'description',
            'price',
            'discount_price'
        ]

We can now use CourseSerializer in our api apps views to serialize Course model instances. For example:

# api/views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response

from courses.models import Course
from courses.serializers import CourseSerializer


@api_view(["GET"])
def api_random(request, *args, **kwargs):
    """
    Django Rest Framework API View
    """
    instance = Course.objects.all().order_by("?").first()
    data = {}
    if instance:
        data = CourseSerializer(instance).data
    return Response(data)

Let's run out client.py script again to consume this updated endpoint, we can see that the property discount_price which was created using the @property decorator is also available:

Now we have installed DRF, let's navigate to our random endpoint: http://127.0.0.1:8000/api/random/, you will see a styled page instead of just raw JSON data, this is because of the Django REST Framework's browsable API feature. The browsable API is another powerful feature of DRF that provides a user-friendly interface for browsing and interacting with your API endpoints directly from a web browser.

DRF automatically generates this browsable interface for your API views if you are using the default settings. When you access a DRF API endpoint in a web browser, DRF recognizes the request and returns the HTML representation of the browsable API interface instead of just raw JSON data.

This feature is helpful during API development and debugging as it allows developers to inspect API responses visually and interact with different endpoints without needing external tools.

In production environments, you may want to disable the browsable API for security reasons and return only JSON responses.

To disable the browsable API in production environments and return only JSON responses, you can configure your Django REST Framework settings to exclude the browsable renderer. Simply add this code snippet to your Django project's settings file (settings.py).

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
    ],
    # Other DRF settings...
}

By making these changes, your Django REST Framework API will no longer render the browsable HTML interface in production environments, and it will return JSON responses exclusively. This helps improve security by minimising the amount of information exposed to potential attackers.


Another benefit of using DRF's serializers, is that we can easily customise the JSON representation of our models. We can customise the serialization process in Django REST Framework (DRF) to rename a property in the serialized JSON response without modifying the original model.

Let's rename the discount_price property of a Course model to sale in the JSON output. This can sometimes be useful for enhancing the readability or clarity of your API responses.

We can customise the serialization to rename discount_price to sale by refactoring our courses app serializers.py file as follows:

# courses/serializers.py
from rest_framework import serializers
from .models import Course

# Serializer class for the Course model
class CourseSerializer(serializers.ModelSerializer):
    # Custom field 'sale' to represent discounted price
    sale = serializers.SerializerMethodField(read_only=True)

    # Meta class specifying the model and fields for serialization
    class Meta:
        model = Course
        fields = [
            'id',             
            'title',          
            'course_id',      # Course ID (e.g., CM1010)
            'description',    
            'price',          # Course price
            'sale'            # Discounted price
        ]

    # Method to calculate the discounted price
    def get_sale(self, obj):
        try:
            return obj.discount_price
        except:
            return None

After updating the serializer, run the client.py script again to test the endpoint. The output should include the sale field with the correct discounted price.

In the browser, if we navigate to http://127.0.0.1:8000/api/random/ we can also see the updated JSON:

Customizing Django REST Framework serialization to rename model properties in JSON responses can enhance readability. It also maintains consistency, aligns with domain terminology, abstracts implementation details, and improves compatibility, resulting in a more user-friendly API.

Multiple serializers can be used for the same model to accommodate different use cases or API endpoints. For example, you might have one serializer for creating or updating model instances with strict validation rules, and another serializer for retrieving model instances with additional fields or customisation. This approach allows for flexibility and separation of concerns in your API design.


Posting Data to Django Rest Framework Endpoints

So far we have seen how to retrieving data from a server. In the previous sections of this tutorial series, we've covered how to define models, create serializers to transform model instances into JSON representations, and use views to handle incoming HTTP requests and return appropriate responses. Additionally, we've examined the importance of customising serialization to align with domain terminology and improve the readability and consistency of our API responses. Now, armed with this knowledge, we're ready to dive deeper into the practical aspect of ingesting data into our DRF endpoints.

Let's explore the process of submitting data to a DRF endpoint. Whether you're creating a new resource or updating an existing one, the ability to ingest data into your application's API is fundamental.

First, we'll create a new view function in our views.py file to handle POST requests and for simplicity our api_create view returns the same data in the response, effectively echoing back whatever JSON data was sent to it.

In our api apps views.py we will create a new view using the @api_view decorator. This is a decorator provided by Django REST Framework (DRF). It allows you to specify which HTTP methods your view should respond to (e.g., GET, POST, PUT, DELETE). In this case, it is used to specify that the view will handle POST requests:

# api/views.py

@api_view(["POST"])
def api_create(request):
    """
    DRF API View to create a new course instance.
    """
    data = request.data
    return Response(data)

The Response class in DRF is a powerful and flexible way to return data from your API views. While it serves a similar purpose to Django's built-in HttpResponse class, Response is specifically designed to work seamlessly with RESTful APIs, offering a variety of features that make it more suitable for this purpose.

Now that we have created our DRF API view function called api_create we need to add the view to our api app urls.py as follows:

# api/urls.py
from django.urls import path
from .views import hello_world, api_random, api_create

urlpatterns = [
    path('', hello_world, name='hello_world'),
    path('random/', api_random, name='random'),
    path('create/', api_create, name='create'),
]

Let's update our client.py to send a POST request to the /create/ endpoint of our API. We first need to create a dictionary containing the data for the new course we want to create. This includes fields like title, course_id, description, and price.

# py_consumer/client.py
import requests

# Base URL
BASE_URL = 'http://localhost:8000/api'

# Define the data for the new course
new_course_data = {
    'title': 'Computational Mathematics',
    'course_id': 'CM1015',
    'description': 'How to do arithmetic in different bases',
    'price': 100.00
}

We also need to constructs the full URL for the API endpoint where the new course will be created by appending /create/ to the BASE_URL.

# URL for the endpoint to create a new course
CREATE_COURSE_URL = f'{BASE_URL}/create/'

Finally we sends a POST request to the CREATE_COURSE_URL endpoint with the new_course_data dictionary as the JSON payload. This request is intended to create a new course on the server.

# Check if the request was successful (status code 200)
if response.status_code == 200:
    print('New course created successfully!')
    print('New course data:', response.json())
else:
    print(f'Error: {response.status_code}')

Now, when we run our client.py, the script sends a HTTP POST request to the CREATE_COURSE_URL endpoint with the new_course_data dictionary as the JSON payload using:
requests.post(CREATE_COURSE_URL, json=new_course_data).

Right now when a POST request is made to our api_create view, with some JSON data, our function captures that data and then immediately sends it back in the response. We are not validating this data.

One of the great benefits of using Django Rest Framework (DRF) serializers is their built-in support for data validation. By defining serializers for your models and using them in your views, you gain automatic validation of incoming data according to the rules you specify in the serializer class.

To save a new course to our database, refactor the api_create view as follows:

# api/views.py

@api_view(["POST"])
def api_create(request):
    """
    DRF API View to create a new course instance.
    """
    serializer = CourseSerializer(data=request.data)
    if serializer.is_valid(raise_exception=True):
        data = serializer.save()  # Save the serializer and get the instance
        return Response(serializer.data)

Let's break down the code step by step:

  • @api_view(["POST"]): This is a decorator provided by Django REST Framework (DRF) to specify that this function-based view is intended to handle HTTP POST requests. It ensures that the view only responds to POST requests.

  • def api_create(request): This is the view function that will handle the incoming POST request. It takes the request object as its argument, representing the HTTP request received by the server.

  • serializer = CourseSerializer(data=request.data) - Here, a serializer instance is created using the CourseSerializer class. The data parameter is set to request.data, which contains the data sent in the POST request body. This line initialises the serializer with the data to be validated and processed.

  • if serializer.is_valid(raise_exception=True) - This line checks if the data passed to the serializer is valid according to the serializer's validation rules. If raise_exception is set to True, any validation errors will raise an exception rather than returning a boolean value.

  • data = serializer.save(): If the data is valid, the save() method of the serializer is called. This method validates the data again (to handle any edge cases) and then creates or updates an object instance based on the validated data. The returned value (data) is the saved instance.

  • return Response(serializer.data) - Finally, if everything is successful, a HTTP response is returned with the serialized data of the saved instance. The serializer.data property contains the serialized representation of the instance, which will be converted to JSON format and returned in the response body.

So our view function validates incoming POST request data using a serializer, saves the validated data to the database, and returns a JSON response containing the serialized data of the saved instance. If there are any validation errors, an exception will be raised, which can be handled gracefully by Django REST Framework.

To test the validation, try changing the test data:

# Define the data for the new course
new_course_data = {
    'title': 'Computational Mathematics',
    'course_id': 'CM1015',
    'description': 'How to do arithmetic in different bases',
    'price': 100.00  # Specify the price
}

Here are some examples of test data you can use to test different validation scenarios for your new course creation API endpoint:

  1. Missing Required Fields:
new_course_data = {
    # Missing 'title' field
    'course_id': 'CM1015',
    'description': 'How to do arithmetic in different bases',
    'price': 100.00
}

  1. Data Length Exceeds Maximum Length:
new_course_data = {
    'title': 'A' * 121,  # 'title' exceeds maximum length of 120 characters
    'course_id': 'CM1015',
    'description': 'How to do arithmetic in different bases',
    'price': 100.00
}
  1. Data Length Does Not Meet Minimum Lengt
new_course_data = {
    'title': '',  # Empty 'title'
    'course_id': 'CM1015',
    'description': 'How to do arithmetic in different bases',
    'price': 100.00
}

To enforce strict validation for the data types of fields, you can specify the field types in your serializer or create custom validation methods.

Here's how you can update your serializer to enforce string data type for the course_id field:

# courses/serializers.py
from rest_framework import serializers
from .models import Course

class CourseSerializer(serializers.ModelSerializer):
    # Explicitly specify field types to enforce 
    # strict validation
    course_id = serializers.CharField(max_length=20)  # Ensure course_id is a string

    class Meta:
        model = Course
        fields = ['id', 'title', 'course_id', 'description', 'price', 'sale']

With the above modification, the serializer will raise a validation error if the course_id field is provided as an integer instead of a string, ensuring stricter validation of the input data types.

Now navigate to http://127.0.0.1:8000/api/create/ and POST the following JSON:

{
    "title": "Computational Mathematics",
    "course_id": "CM1015",
    "description": "How to do arithmetic in different bases",
    "price": "100.00"
}

After making the post request, you should see that the post was successful and a new course was added to the database.


Django Rest Framework Generic views

Frequently used views that map closely to database models (such as creating a model instance, deleting it, listing instances, etc.) are already pre-built in Django REST Framework Views. DRF provides a set of powerful, high-level views and viewsets called Generic views. These views encapsulate common patterns for creating, retrieving, updating, and deleting resources. They are designed to reduce the amount of boilerplate code you need to write for typical API endpoints.

The generic views provided by REST framework allow you to quickly build API views that map closely to your database models. By leveraging DRF Generic API views, you can rapidly develop robust and consistent APIs, focusing on business logic rather than repetitive code.

Let's first use the generics.RetrieveAPIView to create a Django REST Framework class based view that retrieves and returns the details of a single Course. Inside our api app views.py add the following code:

# api/views.py
from rest_framework import generics
from .models import Course
from .serializers import CourseSerializer

class CourseDetailAPIView(generics.RetrieveAPIView):
    queryset = Course.objects.all()
    serializer_class = CourseSerializer

CourseDetailAPIView = CourseDetailAPIView.as_view()

The CourseDetailAPIView class, which inherits from generics.RetrieveAPIView, is used to handle GET requests for retrieving a single Course instance using the CourseSerializer, and the queryset defines the data source for the view, which is all Course instances.

The CourseDetailAPIView class is a class-based view provided by Django REST Framework (DRF). This class-based view is then converted into an asynchronous view function using the as_view() method. This conversion allows the view to be used in URL configurations, making it possible for the view to handle HTTP requests. In URL configurations, this view function can be mapped to a specific URL pattern, enabling the server to route incoming HTTP GET requests to this view, which will retrieve and return the details of a specific Course instance.

The as_view() method converts this class-based view into an asynchronous view function, which can then be used in URL configurations to handle incoming HTTP requests. This setup allows for the retrieval and detailed display of a specific Course instance based on the provided primary key (pk).

Now we have setup the view, remember to also update the api apps urls.py:

# api/urls.py
from django.urls import path
from .views import hello_world, api_random, api_create

from .views import CourseDetailAPIView

urlpatterns = [
    path('', hello_world, name='hello_world'),
    path('random/', api_random, name='random'),
    path('create/', api_create, name='create'),
    #DRF Generic views
    # generics.RetrieveAPIView, is a DRF class specifically designed to handle 
    # HTTP GET requests for retrieving a single model instance. 
    path('courses/<int:pk>/', CourseDetailAPIView, name='course-detail'),
]

In our urls.py we map the URL pattern courses/<int:pk>/ to the CourseDetailAPIView. When a GET request is made to a URL matching this pattern, the CourseDetailAPIView will retrieve and return the details of the Course instance with the specified primary key (pk).

For best practice and consistency with RESTful API conventions, our detail view URL should follow the plural form of the resource name. In this case, "courses" is the plural form, which aligns with common RESTful practices.
he URL name course-detail can be used to reference this route in a convenient and consistent manner throughout our Django project.

In our py_consumer directory, lets create a new file called details.py to consume this new endpoint. Our script will send a GET request to the http://127.0.0.1:8000/api/courses/9/ endpoint to retrieve details about a specific course with pk=9:

import requests

# Base URL
BASE_URL = 'http://localhost:8000/api/courses'

#retrieves the details of the resource with the ID 9
DETAIL_URL = f'{BASE_URL}/9/'

get_response = requests.get(DETAIL_URL) 

# Check if the request was successful (status code 200)
if get_response.status_code == 200:
    try:
        # Attempt to parse the response as JSON
        response_data = get_response.json()
        print(response_data)
    except ValueError:
        # If parsing the response as JSON fails, print the raw response content
        print("Response content is not valid JSON:", get_response.content)
else:
    # Print an error message with the status code
    print(f'Error: {get_response.status_code}')
    print("Response content:", get_response.content)

You can also view this endpoint using the DRF's browsable API by simply navigating to http://127.0.0.1:8000/api/courses/9/


Now let's implement a create view using Django REST Framework. This view allows us to send a POST requests with course data to create new courses in our database. In our api app views.py we need to define a class-based view that inherits from generics.CreateAPIView:

# api/views.py
from rest_framework import generics
from .models import Course
from .serializers import CourseSerializer

class CourseCreateAPIView(generics.CreateAPIView):
    queryset = Course.objects.all()
    serializer_class = CourseSerializer

CourseCreateAPIView = CourseCreateAPIView.as_view()

NOTE: When a POST request is made to a view that inherits from CreateAPIView, DRF automatically invokes the perform_create method.
The perform_create provides a flexible and customisable way to handle object creation in DRF views, allowing you to tailor the creation process to your specific requirements and business logic.

again remember to update the api apps urlpatterns too.

# api/urls.py
urlpatterns = [
    # Other URL patterns...
    path('courses/create/', CourseCreateAPIView, name='course-create'),
]

and in our py_consumer directory create a new create.py file to test our endpoint as follows:

#py_consumer/create.py
import requests

# Base URL
BASE_URL = 'http://localhost:8000/api/courses'

CREATE_URL = f'{BASE_URL}/create/'

# Define the data for the new course
new_course_data = {
    'title': 'Computational Mathematics',
    'course_id': 'CM1015',
    'description': 'How to do arithmetic in different bases',
    'price': 100.00  # Specify the price
}

# Make a POST request to create a new course
post_response = requests.post(CREATE_URL, json=new_course_data)

# Check if the request was successful (status code 201)
if post_response.status_code == 201:
    try:
        # Attempt to parse the response as JSON
        response_data = post_response.json()
        print('New course created successfully!')
        print('New course data:', response_data)
    except ValueError:
        # If parsing the response as JSON fails, 
        # print the raw response content
        print("Response content is not valid JSON:", post_response.content)
else:
    # Print an error message with the status code
    print(f'Error: {post_response.status_code}')
    print("Response content:", post_response.content)

Now run the create.py script. When you run this script, it sends a POST request to the specified API endpoint to create a new course with the provided data and prints the response or an error message based on the outcome.

Again, you can also use the DRF's browsable API to add a course. In the DRF browsable API interface, you will see a form where you can input data for a new course. You can copy and paste the JSON payload into the corresponding fields.

#Example JSON Payload
{
    "title": "Computational Mathematics",
    "course_id": "CM1015",
    "description": "How to do arithmetic in different bases",
    "price": 100.00
}

After submitting the form, you should receive a response indicating that the new course has been successfully created. If you are using the DRF browsable API, the response will be displayed in the interface, showing the details of the newly created course:


Alternative: Using cURL or HTTPie

If you prefer to test the API using command-line tools like cURL or HTTPie, you can use the following commands:

Using cURL:

curl -X POST http://localhost:8000/api/courses/create/ \
     -H "Content-Type: application/json" \
     -d '{"title": "Computational Mathematics", "course_id": "CM1015", "description": "How to do arithmetic in different bases", "price": 100.00}'

Using HTTPie:

HTTPie is a command-line tool for making HTTP requests. It provides a simple and intuitive interface for interacting with HTTP servers, allowing users to easily send requests, view responses, and perform various HTTP operations such as GET, POST, PUT, DELETE, etc. Make sure you have HTTPie installed first - pip install httpie

http POST http://localhost:8000/api/courses/create/ \
     title="Computational Mathematics" \
     course_id="CM1015" \
     description="How to do arithmetic in different bases" \
     price:=100.00

These commands send a POST request to the create endpoint with the JSON payload, simulating the same process as using the DRF browsable API.


In a similar manner, DRF makes it very easy to create a list view for all the courses in out datravase, let's implement a CourseListAPIView by defining a a class named CourseListAPIView that inherits from generics.ListAPIView. This class will handle listing all the courses:

# api/views.py
class CourseListAPIView(generics.ListAPIView):
    queryset = Course.objects.all()
    serializer_class = CourseSerializer

CourseListAPIView = CourseListAPIView.as_view()

Now, we can include CourseListAPIView in our URL patterns to route requests to this view, allowing users to retrieve a list of all courses.


# api/urls.py
urlpatterns = [
    # Other URL patterns...
    path('courses/', CourseListAPIView, name='course-list'),
]

With this URL pattern in place, requests to the '/api/courses/' endpoint will be routed to the CourseListAPIView, allowing users to retrieve a list of all courses.

And here's how we can create a Python script to consume the CourseListAPIView. Inside in our py_consumer directory create a new list.py file to test our endpoint as follows:

#py_consumer/create.py
import requests

# Base URL
BASE_URL = 'http://localhost:8000/api/courses/'

# Make a GET request to retrieve a list of courses
response = requests.get(BASE_URL)

# Check if the request was successful (status code 200)
if response.status_code == 200:
    try:
        # Attempt to parse the response as JSON
        courses = response.json()
        print('List of Courses:')
        for course in courses:
            print(course)
    except ValueError:
        # If parsing the response as JSON fails, print the raw response content
        print("Response content is not valid JSON:", response.content)
else:
    # Print an error message with the status code
    print(f'Error: {response.status_code}')
    print("Response content:", response.content)

When you run the list script, it sends a GET request to retrieve a list of courses from the specified API endpoint and prints the response data. Alternatively, navigate to http://127.0.0.1:8000/api/courses/ to see the list of courses in the DRF browsable API.


Alternatively you can use CourseListCreateAPIView instead of CourseListAPIView. CourseListCreateAPIView provides both list and create functionalities in a single view. It allows you to retrieve a list of courses and create new courses in the same view. This can be convenient if you want to handle both operations in one endpoint.

# api/views.py

class CourseListCreateAPIView(generics.ListCreateAPIView):
    queryset = Course.objects.all()
    serializer_class = CourseSerializer

CourseListCreateAPIView = CourseListCreateAPIView.as_view()

This creates a new API endpoint for both listing all courses and creating new courses.


Resources

1
Subscribe to my newsletter

Read articles from Pedram Badakhchani directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Pedram Badakhchani
Pedram Badakhchani

Working as an online tutor for the Bachelor of Computer Science Degree from Goldsmiths, University of London on the Coursera platform. This is the first undergraduate degree programme available on Coursera, one of the world’s leading online education providers. The programme has been designed to equip students to access careers in emerging technologies, providing opportunities for students to study machine learning, data science, virtual reality, game development and web programming to meet the needs of career changers in industry as well as those taking their first steps into the innovative computer science field.