Thanks! We'll be in touch in the next 12 hours
Oops! Something went wrong while submitting the form.

Using DRF Effectively to Build Cleaner and Faster APIs in Django

Django REST Framework (DRF) is a popular library choice when it comes to creating REST APIs with Django. With minimal effort and time, you can start creating APIs that support authentication, authorization, pagination, sorting, etc. Once we start creating production-level APIs, we must do a lot of customization that are highly supported by DRF.

In this blog post, I will share some of the features that I have used extensively while working with DRF. We will be covering the following use cases:

  1. Using serializer context to pass data from view to serializer
  2. Handling reverse relationships in serializers
  3. Solving slow queries by eliminating the N+1 query problem
  4. Custom Response Format
  5. SerializerMethodField to add read-only derived data to the response
  6. Using Mixin to enable/disable pagination with Query Param

This will help you to write cleaner code and improve API performance.

Prerequisite:

To understand the things discussed in the blog, the reader should have some prior experience of creating REST APIs using DRF. We will not be covering the basic concepts like serializers, API view/viewsets, generic views, permissions, etc. If you need help in building the basics, here is the list of resources from official documentation.

Let’s explore Django REST Framework’s (DRF) lesser-known but useful features:

1. Using Serializer Context to Pass Data from View to Serializer

Let us consider a case when we need to write some complex validation logic in the serializer. 

The validation method takes two parameters. One is the self or the serializer object, and the other is the field value received in the request payload. Our validation logic may sometimes need some extra information that must be taken from the database or derived from the view calling the serializer. 

Next is the role of the serializer’s context data. The serializer takes the context parameter in the form of a python dictionary, and this data is available throughout the serializer methods. The context data can be accessed using self.context in serializer validation methods or any other serializer method. 

Passing custom context data to the serializer

To pass the context to the serializer, create a dictionary with the data and pass it in the context parameter when initializing the serializer.

CODE: https://gist.github.com/velotiotech/c272e28ee0a6e4b97a9a251083cbb109.js

In case of generic view and viewsets, the serializer initialization is handled by the framework and passed the following as default context.

CODE: https://gist.github.com/velotiotech/7d085ff4c63d53f1adb487e40d1dc926.js

Thanks to DRF, we can cleanly and easily customize the context data. 

CODE: https://gist.github.com/velotiotech/a00f0095bd859b9c29ff38e3f27e7258.js

CODE: https://gist.github.com/velotiotech/e2d8deff68d4beef99f1c2c1aafb7533.js

2. Handling Reverse Relationships in Serializers 

To better understand this, take the following example. 

CODE: https://gist.github.com/velotiotech/ffc03ca7036d32cde8498255edfacbcd.js

We have a User model, which contains data about the customer and Address that has the list of addresses added. We need to return the user details along with their address detail, as given below.

CODE: https://gist.github.com/velotiotech/0f91cd944a46d58a342e43f1f8a25bca.js

  • Forward model relationships are automatically included in the fields returned by the ModelSerializer.
  • The relationship between User and Address is a reverse relationship and needs to be explicitly added in the fields. 
  • We have defined a related_name=addresses for the User Foreign Key in the Address; it can be used in the fields meta option. 
  • If we don’t have the related_name, we can use address_set, which is the default related_name.

CODE: https://gist.github.com/velotiotech/3945d12cb7227a5e192176a209ce29b7.js

The above code will return the following response:

CODE: https://gist.github.com/velotiotech/4c47b0bae7dac4483b68c9fdba6c65d5.js

But this isn’t what we need. We want to return all the information about the address and not just the IDs. DRF gives us the ability to use a serializer as a field to another serializer. 

The below code shows how to use the nested Serializer to return the address details.

CODE: https://gist.github.com/velotiotech/f488aa2244f73ec8032bd323e6b7417e.js

  • The read_only=True parameter marks the field as a read-only field. 
  • The addresses field will only be used in GET calls and will be ignored in write operations. 
  • Nested Serializers can also be used in write operations, but DRF doesn’t handle the creation/deletion of nested serializers by default.

3. Solving Slow Queries by Eliminating the N+1 Query Problem

When using nested serializers, the API needs to run queries over multiple tables and a large number of records. This can often lead to slower APIs. A common and easy mistake to make while using serializer with relationships is the N+1 queries problem. Let’s first understand the problem and ways to solve it.

Identifying the N+1 Queries Problem 

Let’s take the following API example and count the number of queries hitting the database on each API call.

CODE: https://gist.github.com/velotiotech/573b5c4abec5591835b2335e8422a61f.js

CODE: https://gist.github.com/velotiotech/59196c2b39096272e105281dfafbb1c4.js

CODE: https://gist.github.com/velotiotech/c3db8054e631afdfc98cab52232a24fd.js

CODE: https://gist.github.com/velotiotech/f247bbcbfeefab9ace3183c64bf21643.js

We are creating a simple API to list the books along with the author’s details. Here is the output:

CODE: https://gist.github.com/velotiotech/90d5552cbb2ccd200bb851dfcf4c3a46.js

Ideally, we should be able to get data in 1 single SQL query. Now, let’s write a test case and see if our assumption is correct:

CODE: https://gist.github.com/velotiotech/c9b38c604648b4ce25267617fb094fee.js

As we see, our test case has failed, and it shows that the number of queries running are 11 and not one. In our test case, we added 10 records in the Book model. The number of queries hitting the database is 1(to fetch books list) + the number of records in the Book model (to fetch author details for each book record). The test output shows the SQL queries executed. 

The side effects of this can easily go unnoticed while working on a test database with a small number of records. But in production, when the data grows to thousands of records, this can seriously degrade the performance of the database and application.

Let’s Do It the Right Way

If we think this in terms of a raw SQL query, this can be achieved with a simple Inner Join operation between the Book and the Author table. We need to do something similar in our Django query. 

Django provides selected_related and prefetch_related to handle query problems around related objects. 

  • select_related works on forward ForeignKey, OneToOne, and backward OneToOne relationships by creating a database JOIN and fetching the related field data in one single query. 
  • prefetch_related works on forward ManyToMany and in reverse, ManyToMany, ForeignKey. prefetch_related does a different query for every relationship and plays out the "joining" in Python. 

Let’s rewrite the above code using select_related and check the number of queries. 

We only need to change the queryset in the view. 

CODE: https://gist.github.com/velotiotech/310a7446febfcc6fe6813ca293c971ec.js

Now, we will rerun the test, and this time it should pass:

CODE: https://gist.github.com/velotiotech/16212fce9214a12d72c09bcb71ad90cf.js

If you are interested in knowing the SQL query executed, here it is:

CODE: https://gist.github.com/velotiotech/2b2ab3bc53243fffc7b1df021158aff9.js

4. Custom Response Format

It’s a good practice to decide the API endpoints and their request/response payload before starting the actual implementation. If you are the developer, by writing the implementation for the API where the response format is already decided, you can not go with the default response returned by DRF. 

Let’s assume that, below is the decided format for returning the response: 

CODE: https://gist.github.com/velotiotech/845e1311a73d80b71dd332ba9fb7da3f.js

We can see that the response format has a message, errors, status, and data attributes. Next, we will see how to write a custom renderer to achieve the above response format. Since the format is in JSON , we override the rest_framework.renderers.JSONRenderer.

CODE: https://gist.github.com/velotiotech/50e1dc9713879a73cb3765870347b2b0.js

To use this new renderer, we need to add it to  DRF settings:

CODE: https://gist.github.com/velotiotech/85d3238f6fce07b28ff7999a60ae86bf.js

5. Use the SerializerMethodField to add read-only derived data to the response

The SerializerMethodField can be used when we want to add some derived data to the object. Consider the same Book listing API. If we want to send an additional property display name—which is the book name in uppercase—we can use the serializer method field as below.

CODE: https://gist.github.com/velotiotech/54e716e3c1fc1afb6e688291c89dafae.js

  • The SerializerMethodField takes the source parameter, where we can pass the method name that should be called. 
  • The method gets self and the object as the argument.
  • By default, the DRF source parameter uses get_{field_name}, so in the example above, the source parameter can be omitted, and it will still give the same result.

CODE: https://gist.github.com/velotiotech/caedc73fc1593e8d65330016faabcd1b.js

6. Use Mixin to Enable/disable Pagination with Query Param

If you are developing APIs for an internal application and want to support APIs with pagination both enabled and disabled, you can make use of the Mixin below. This allows the caller to use the query parameter “pagination” to enable/disable pagination. This Mixin can be used with the generic views.

CODE: https://gist.github.com/velotiotech/1edb71826b4e1310a98746b7fb0acb1b.js

CODE: https://gist.github.com/velotiotech/392b271c7ac2ebba12f0ccf7721f048e.js

Conclusion

This was just a small selection of all the awesome features provided by Django and DRF, so keep exploring. I hope you learned something new today. If you are interested in learning more about serverless deployment of Django Applications, you can refer to our comprehensive guide to deploy serverless, event-driven Python applications using Zappa.

Further Reading

  1. Django Rest framework Documentation
  2. Django Documentation