• United States+1
  • United Kingdom+44
  • Afghanistan (‫افغانستان‬‎)+93
  • Albania (Shqipëri)+355
  • Algeria (‫الجزائر‬‎)+213
  • American Samoa+1684
  • Andorra+376
  • Angola+244
  • Anguilla+1264
  • Antigua and Barbuda+1268
  • Argentina+54
  • Armenia (Հայաստան)+374
  • Aruba+297
  • Australia+61
  • Austria (Österreich)+43
  • Azerbaijan (Azərbaycan)+994
  • Bahamas+1242
  • Bahrain (‫البحرين‬‎)+973
  • Bangladesh (বাংলাদেশ)+880
  • Barbados+1246
  • Belarus (Беларусь)+375
  • Belgium (België)+32
  • Belize+501
  • Benin (Bénin)+229
  • Bermuda+1441
  • Bhutan (འབྲུག)+975
  • Bolivia+591
  • Bosnia and Herzegovina (Босна и Херцеговина)+387
  • Botswana+267
  • Brazil (Brasil)+55
  • British Indian Ocean Territory+246
  • British Virgin Islands+1284
  • Brunei+673
  • Bulgaria (България)+359
  • Burkina Faso+226
  • Burundi (Uburundi)+257
  • Cambodia (កម្ពុជា)+855
  • Cameroon (Cameroun)+237
  • Canada+1
  • Cape Verde (Kabu Verdi)+238
  • Caribbean Netherlands+599
  • Cayman Islands+1345
  • Central African Republic (République centrafricaine)+236
  • Chad (Tchad)+235
  • Chile+56
  • China (中国)+86
  • Christmas Island+61
  • Cocos (Keeling) Islands+61
  • Colombia+57
  • Comoros (‫جزر القمر‬‎)+269
  • Congo (DRC) (Jamhuri ya Kidemokrasia ya Kongo)+243
  • Congo (Republic) (Congo-Brazzaville)+242
  • Cook Islands+682
  • Costa Rica+506
  • Côte d’Ivoire+225
  • Croatia (Hrvatska)+385
  • Cuba+53
  • Curaçao+599
  • Cyprus (Κύπρος)+357
  • Czech Republic (Česká republika)+420
  • Denmark (Danmark)+45
  • Djibouti+253
  • Dominica+1767
  • Dominican Republic (República Dominicana)+1
  • Ecuador+593
  • Egypt (‫مصر‬‎)+20
  • El Salvador+503
  • Equatorial Guinea (Guinea Ecuatorial)+240
  • Eritrea+291
  • Estonia (Eesti)+372
  • Ethiopia+251
  • Falkland Islands (Islas Malvinas)+500
  • Faroe Islands (Føroyar)+298
  • Fiji+679
  • Finland (Suomi)+358
  • France+33
  • French Guiana (Guyane française)+594
  • French Polynesia (Polynésie française)+689
  • Gabon+241
  • Gambia+220
  • Georgia (საქართველო)+995
  • Germany (Deutschland)+49
  • Ghana (Gaana)+233
  • Gibraltar+350
  • Greece (Ελλάδα)+30
  • Greenland (Kalaallit Nunaat)+299
  • Grenada+1473
  • Guadeloupe+590
  • Guam+1671
  • Guatemala+502
  • Guernsey+44
  • Guinea (Guinée)+224
  • Guinea-Bissau (Guiné Bissau)+245
  • Guyana+592
  • Haiti+509
  • Honduras+504
  • Hong Kong (香港)+852
  • Hungary (Magyarország)+36
  • Iceland (Ísland)+354
  • India (भारत)+91
  • Indonesia+62
  • Iran (‫ایران‬‎)+98
  • Iraq (‫العراق‬‎)+964
  • Ireland+353
  • Isle of Man+44
  • Israel (‫ישראל‬‎)+972
  • Italy (Italia)+39
  • Jamaica+1876
  • Japan (日本)+81
  • Jersey+44
  • Jordan (‫الأردن‬‎)+962
  • Kazakhstan (Казахстан)+7
  • Kenya+254
  • Kiribati+686
  • Kosovo+383
  • Kuwait (‫الكويت‬‎)+965
  • Kyrgyzstan (Кыргызстан)+996
  • Laos (ລາວ)+856
  • Latvia (Latvija)+371
  • Lebanon (‫لبنان‬‎)+961
  • Lesotho+266
  • Liberia+231
  • Libya (‫ليبيا‬‎)+218
  • Liechtenstein+423
  • Lithuania (Lietuva)+370
  • Luxembourg+352
  • Macau (澳門)+853
  • Macedonia (FYROM) (Македонија)+389
  • Madagascar (Madagasikara)+261
  • Malawi+265
  • Malaysia+60
  • Maldives+960
  • Mali+223
  • Malta+356
  • Marshall Islands+692
  • Martinique+596
  • Mauritania (‫موريتانيا‬‎)+222
  • Mauritius (Moris)+230
  • Mayotte+262
  • Mexico (México)+52
  • Micronesia+691
  • Moldova (Republica Moldova)+373
  • Monaco+377
  • Mongolia (Монгол)+976
  • Montenegro (Crna Gora)+382
  • Montserrat+1664
  • Morocco (‫المغرب‬‎)+212
  • Mozambique (Moçambique)+258
  • Myanmar (Burma) (မြန်မာ)+95
  • Namibia (Namibië)+264
  • Nauru+674
  • Nepal (नेपाल)+977
  • Netherlands (Nederland)+31
  • New Caledonia (Nouvelle-Calédonie)+687
  • New Zealand+64
  • Nicaragua+505
  • Niger (Nijar)+227
  • Nigeria+234
  • Niue+683
  • Norfolk Island+672
  • North Korea (조선 민주주의 인민 공화국)+850
  • Northern Mariana Islands+1670
  • Norway (Norge)+47
  • Oman (‫عُمان‬‎)+968
  • Pakistan (‫پاکستان‬‎)+92
  • Palau+680
  • Palestine (‫فلسطين‬‎)+970
  • Panama (Panamá)+507
  • Papua New Guinea+675
  • Paraguay+595
  • Peru (Perú)+51
  • Philippines+63
  • Poland (Polska)+48
  • Portugal+351
  • Puerto Rico+1
  • Qatar (‫قطر‬‎)+974
  • Réunion (La Réunion)+262
  • Romania (România)+40
  • Russia (Россия)+7
  • Rwanda+250
  • Saint Barthélemy (Saint-Barthélemy)+590
  • Saint Helena+290
  • Saint Kitts and Nevis+1869
  • Saint Lucia+1758
  • Saint Martin (Saint-Martin (partie française))+590
  • Saint Pierre and Miquelon (Saint-Pierre-et-Miquelon)+508
  • Saint Vincent and the Grenadines+1784
  • Samoa+685
  • San Marino+378
  • São Tomé and Príncipe (São Tomé e Príncipe)+239
  • Saudi Arabia (‫المملكة العربية السعودية‬‎)+966
  • Senegal (Sénégal)+221
  • Serbia (Србија)+381
  • Seychelles+248
  • Sierra Leone+232
  • Singapore+65
  • Sint Maarten+1721
  • Slovakia (Slovensko)+421
  • Slovenia (Slovenija)+386
  • Solomon Islands+677
  • Somalia (Soomaaliya)+252
  • South Africa+27
  • South Korea (대한민국)+82
  • South Sudan (‫جنوب السودان‬‎)+211
  • Spain (España)+34
  • Sri Lanka (ශ්‍රී ලංකාව)+94
  • Sudan (‫السودان‬‎)+249
  • Suriname+597
  • Svalbard and Jan Mayen+47
  • Swaziland+268
  • Sweden (Sverige)+46
  • Switzerland (Schweiz)+41
  • Syria (‫سوريا‬‎)+963
  • Taiwan (台灣)+886
  • Tajikistan+992
  • Tanzania+255
  • Thailand (ไทย)+66
  • Timor-Leste+670
  • Togo+228
  • Tokelau+690
  • Tonga+676
  • Trinidad and Tobago+1868
  • Tunisia (‫تونس‬‎)+216
  • Turkey (Türkiye)+90
  • Turkmenistan+993
  • Turks and Caicos Islands+1649
  • Tuvalu+688
  • U.S. Virgin Islands+1340
  • Uganda+256
  • Ukraine (Україна)+380
  • United Arab Emirates (‫الإمارات العربية المتحدة‬‎)+971
  • United Kingdom+44
  • United States+1
  • Uruguay+598
  • Uzbekistan (Oʻzbekiston)+998
  • Vanuatu+678
  • Vatican City (Città del Vaticano)+39
  • Venezuela+58
  • Vietnam (Việt Nam)+84
  • Wallis and Futuna+681
  • Western Sahara (‫الصحراء الغربية‬‎)+212
  • Yemen (‫اليمن‬‎)+967
  • Zambia+260
  • Zimbabwe+263
  • Åland Islands+358
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

Atul Mishra

Full-stack Development

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.

context_data = {"valid_domains": ValidDomain.objects.all()}
serializer = MySerializer(data=request.data, context=context_data)
view raw context.py hosted with ❤ by GitHub

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

{
'request': self.request,
'format': self.format_kwarg,
'view': self
}

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

# override the get_serializer_context method in the generic viewset
class UserCreateListAPIView(generice.ListCreateAPIView):
def get_serializer_context(self):
context = super().get_serializer_context()
# Update context data to add new data
context.update({"valid_domains": ValidDomain.objects.all()})
return context
view raw views.py hosted with ❤ by GitHub

# read the context data in the serializer validation method
class UserSerializer(serializer.Serializer):
def validate_email(self, val):
valid_domains = serf.context.get("valid_domains")
# main validation logic goes here
view raw serializer.py hosted with ❤ by GitHub

2. Handling Reverse Relationships in Serializers 

To better understand this, take the following example. 

class User(models.Model):
name = models.CharField(max_length=60)
email = models.EmailField()
class Address(models.Model):
detail = models.CharField(max_length=100)
city = models.FloatField()
user = models.ForeignKey(User, related_name="addresses", on_delete=models.CASCADE)
view raw models.py hosted with ❤ by GitHub

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.

{
"name": "Velotio",
"email": "velotio@example.com",
"addresses": [
{
"detail": "Akshya Nagar 1st Block 1st Cross, Rammurthy nagar",
"city": "Banglore"
},
{
"detail": "50 nd Floor, , Narayan Dhuru Street, Mandvi",
"city": "Mumbai"
},
{
"detail": "Ground Floor, 8/5, J K Bldg, H G Marg, Opp Gamdevi Temple, Grant Road",
"city": "Banglore"
}
]
}
view raw response.json hosted with ❤ by GitHub

  • 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.

class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("name", "email", "addresses")
view raw serializer.py hosted with ❤ by GitHub

The above code will return the following response:

{
"name": "Velotio",
"email": "velotio@example.com",
"addresses": [
10,
20,
45
]
}
view raw response.json hosted with ❤ by GitHub

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.

class AddressSerializer(serializers.ModelSerializer):
class Meta:
model = Address
fields = ("detail", "city")
class UserSerializer(serializers.ModelSerializer):
addresses = AddressSerializer(many=True, read_only=True)
class Meta:
model = User
fields = ("name", "email", "addresses")
view raw serializer.py hosted with ❤ by GitHub

  • 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.

class Author(models.Model):
name = models.CharField(max_length=20)
class Book(models.Model):
name = models.CharField(max_length=20)
author = models.ForeignKey("Author", models.CASCADE, related_name="books")
created_at = models.DateTimeField(auto_now_add=True)
view raw models.py hosted with ❤ by GitHub

class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = "__all__"
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer()
class Meta:
model = Book
fields = "__all__"
view raw serializer.py hosted with ❤ by GitHub

class BookListCreateAPIView(generics.ListCreateAPIView):
serializer_class = BookSerializer
queryset = Book.objects.all()
view raw views.py hosted with ❤ by GitHub

urlpatterns = [
path('admin/', admin.site.urls),
path('hello-world/', HelloWorldAPI.as_view()),
path('books/', BookListCreateAPIView.as_view(), name="book_list")
]
view raw urls.py hosted with ❤ by GitHub

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

{
"message": "",
"errors": [],
"data": [
{
"id": 1,
"author": {
"id": 3,
"name": "Meet teacher."
},
"name": "Body society.",
"created_at": "1973-08-03T02:43:22Z"
},
{
"id": 2,
"author": {
"id": 49,
"name": "Cause wait health."
},
"name": "Left next pretty.",
"created_at": "2000-07-07T03:37:10Z"
},
{
"id": 3,
"author": {
"id": 7,
"name": "No figure those."
},
"name": "Reflect American.",
"created_at": "1994-08-14T03:54:38Z"
},
{
"id": 4,
"author": {
"id": 35,
"name": "Garden order table."
},
"name": "Throw minute.",
"created_at": "1993-12-30T20:50:56Z"
},
{
"id": 5,
"author": {
"id": 49,
"name": "Cause wait health."
},
"name": "Congress now build.",
"created_at": "1977-07-21T17:35:42Z"
},
{
"id": 6,
"author": {
"id": 39,
"name": "Involve section."
},
"name": "Activity drop fight.",
"created_at": "2011-04-21T23:09:54Z"
},
{
"id": 7,
"author": {
"id": 44,
"name": "Cost spring our."
},
"name": "Because pattern.",
"created_at": "2010-01-04T08:21:29Z"
},
{
"id": 8,
"author": {
"id": 45,
"name": "Entire we certainly."
},
"name": "Program use feel.",
"created_at": "1972-11-30T15:49:50Z"
},
{
"id": 9,
"author": {
"id": 42,
"name": "Interest drop."
},
"name": "Purpose live might.",
"created_at": "1987-01-31T16:48:54Z"
},
{
"id": 10,
"author": {
"id": 12,
"name": "Sell data contain."
},
"name": "Everyone thing seem.",
"created_at": "2007-10-19T07:16:34Z"
}
],
"status": "success"
}
view raw response.json hosted with ❤ by GitHub

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:

from django.urls import reverse
from django_seed import Seed
from core.models import Author, Book
from rest_framework.test import APITestCase
seeder = Seed.seeder()
class BooksTestCase(APITestCase):
def test_list_books(self):
# Add dummy data to the Author and Book Table
seeder.add_entity(Author, 5)
seeder.add_entity(Book, 10)
seeder.execute()
# we expect the result in 1 query
with self.assertNumQueries(1):
response = self.client.get(reverse("book_list"), format="json")
# test output
$ ./manage.py test
.
.
.
AssertionError: 11 != 1 : 11 queries executed, 1 expected
Captured queries were:
1. SELECT "core_book"."id", "core_book"."name", "core_book"."author_id", "core_book"."created_at" FROM "core_book"
2. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 4 LIMIT 21
3. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 1 LIMIT 21
4. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 4 LIMIT 21
5. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 4 LIMIT 21
6. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 5 LIMIT 21
7. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 5 LIMIT 21
8. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 1 LIMIT 21
9. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 3 LIMIT 21
10. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 3 LIMIT 21
11. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 5 LIMIT 21
----------------------------------------------------------------------
Ran 1 test in 0.027s
FAILED (failures=1)
view raw tests.py hosted with ❤ by GitHub

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. 

class BookListCreateAPIView(generics.ListCreateAPIView):
serializer_class = BookSerializer
def get_queryset(self):
queryset = Book.objects.select_related("author").all()
return queryset
view raw views.py hosted with ❤ by GitHub

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

$ ./manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.024s
OK
Destroying test database for alias 'default'...
view raw test_output.sh hosted with ❤ by GitHub

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

>> queryset = Book.objects.select_related("author").all()
>> print(queryset.query)
SELECT "core_book"."id",
"core_book"."name",
"core_book"."author_id",
"core_book"."created_at",
"core_author"."id",
"core_author"."name"
FROM "core_book"
INNER JOIN "core_author" ON ("core_book"."author_id" = "core_author"."id")
view raw query.py hosted with ❤ by GitHub

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: 

{
"message": "",
"errors": [],
"data": [
{
"id": 1,
"author": {
"id": 3,
"name": "Meet teacher."
},
"name": "Body society.",
"created_at": "1973-08-03T02:43:22Z"
},
{
"id": 2,
"author": {
"id": 49,
"name": "Cause wait health."
},
"name": "Left next pretty.",
"created_at": "2000-07-07T03:37:10Z"
}
],
"status": "success"
}
view raw output.json hosted with ❤ by GitHub

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.

from rest_framework.renderers import JSONRenderer
from rest_framework.views import exception_handler
class CustomJSONRenderer(JSONRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
# reformat the response
response_data = {"message": "", "errors": [], "data": data, "status": "success"}
# call super to render the response
response = super(CustomJSONRenderer, self).render(
response_data, accepted_media_type, renderer_context
)
return response
view raw renderers.py hosted with ❤ by GitHub

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

REST_FRAMEWORK = {
"DEFAULT_RENDERER_CLASSES": (
"core.renderer.CustomJSONRenderer",
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
)
}
view raw settings.py hosted with ❤ by GitHub

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.

class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer()
book_display_name= serializers.SerializerMethodField(source="get_book_display_name")
def get_book_display_name(self, book):
return book.name.upper()
class Meta:
model = Book
fields = "__all__"
view raw serializer.py hosted with ❤ by GitHub

  • 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.

book_display_name = serializers.SerializerMethodField()

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.

class DynamicPaginationMixin(object):
"""
Controls pagination enable disable option using query param "pagination".
If pagination=false is passed in query params, data is returned without pagination
"""
def paginate_queryset(self, queryset):
pagination = self.request.query_params.get("pagination", "true")
if bool(pagination):
return None
return super().paginate_queryset(queryset)
view raw mixins.py hosted with ❤ by GitHub

# Remember to use mixin before the generics
class BookListCreateAPIView(DynamicPaginationMixin, generics.ListCreateAPIView):
serializer_class = BookSerializer
def get_queryset(self):
queryset = Book.objects.select_related("author").all()
return queryset
view raw views.py hosted with ❤ by GitHub

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
Get the latest engineering blogs delivered straight to your inbox.
No spam. Only expert insights.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Did you like the blog? If yes, we're sure you'll also like to work with the people who write them - our best-in-class engineering team.

We're looking for talented developers who are passionate about new emerging technologies. If that's you, get in touch with us.

Explore current openings

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.

context_data = {"valid_domains": ValidDomain.objects.all()}
serializer = MySerializer(data=request.data, context=context_data)
view raw context.py hosted with ❤ by GitHub

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

{
'request': self.request,
'format': self.format_kwarg,
'view': self
}

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

# override the get_serializer_context method in the generic viewset
class UserCreateListAPIView(generice.ListCreateAPIView):
def get_serializer_context(self):
context = super().get_serializer_context()
# Update context data to add new data
context.update({"valid_domains": ValidDomain.objects.all()})
return context
view raw views.py hosted with ❤ by GitHub

# read the context data in the serializer validation method
class UserSerializer(serializer.Serializer):
def validate_email(self, val):
valid_domains = serf.context.get("valid_domains")
# main validation logic goes here
view raw serializer.py hosted with ❤ by GitHub

2. Handling Reverse Relationships in Serializers 

To better understand this, take the following example. 

class User(models.Model):
name = models.CharField(max_length=60)
email = models.EmailField()
class Address(models.Model):
detail = models.CharField(max_length=100)
city = models.FloatField()
user = models.ForeignKey(User, related_name="addresses", on_delete=models.CASCADE)
view raw models.py hosted with ❤ by GitHub

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.

{
"name": "Velotio",
"email": "velotio@example.com",
"addresses": [
{
"detail": "Akshya Nagar 1st Block 1st Cross, Rammurthy nagar",
"city": "Banglore"
},
{
"detail": "50 nd Floor, , Narayan Dhuru Street, Mandvi",
"city": "Mumbai"
},
{
"detail": "Ground Floor, 8/5, J K Bldg, H G Marg, Opp Gamdevi Temple, Grant Road",
"city": "Banglore"
}
]
}
view raw response.json hosted with ❤ by GitHub

  • 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.

class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ("name", "email", "addresses")
view raw serializer.py hosted with ❤ by GitHub

The above code will return the following response:

{
"name": "Velotio",
"email": "velotio@example.com",
"addresses": [
10,
20,
45
]
}
view raw response.json hosted with ❤ by GitHub

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.

class AddressSerializer(serializers.ModelSerializer):
class Meta:
model = Address
fields = ("detail", "city")
class UserSerializer(serializers.ModelSerializer):
addresses = AddressSerializer(many=True, read_only=True)
class Meta:
model = User
fields = ("name", "email", "addresses")
view raw serializer.py hosted with ❤ by GitHub

  • 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.

class Author(models.Model):
name = models.CharField(max_length=20)
class Book(models.Model):
name = models.CharField(max_length=20)
author = models.ForeignKey("Author", models.CASCADE, related_name="books")
created_at = models.DateTimeField(auto_now_add=True)
view raw models.py hosted with ❤ by GitHub

class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = "__all__"
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer()
class Meta:
model = Book
fields = "__all__"
view raw serializer.py hosted with ❤ by GitHub

class BookListCreateAPIView(generics.ListCreateAPIView):
serializer_class = BookSerializer
queryset = Book.objects.all()
view raw views.py hosted with ❤ by GitHub

urlpatterns = [
path('admin/', admin.site.urls),
path('hello-world/', HelloWorldAPI.as_view()),
path('books/', BookListCreateAPIView.as_view(), name="book_list")
]
view raw urls.py hosted with ❤ by GitHub

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

{
"message": "",
"errors": [],
"data": [
{
"id": 1,
"author": {
"id": 3,
"name": "Meet teacher."
},
"name": "Body society.",
"created_at": "1973-08-03T02:43:22Z"
},
{
"id": 2,
"author": {
"id": 49,
"name": "Cause wait health."
},
"name": "Left next pretty.",
"created_at": "2000-07-07T03:37:10Z"
},
{
"id": 3,
"author": {
"id": 7,
"name": "No figure those."
},
"name": "Reflect American.",
"created_at": "1994-08-14T03:54:38Z"
},
{
"id": 4,
"author": {
"id": 35,
"name": "Garden order table."
},
"name": "Throw minute.",
"created_at": "1993-12-30T20:50:56Z"
},
{
"id": 5,
"author": {
"id": 49,
"name": "Cause wait health."
},
"name": "Congress now build.",
"created_at": "1977-07-21T17:35:42Z"
},
{
"id": 6,
"author": {
"id": 39,
"name": "Involve section."
},
"name": "Activity drop fight.",
"created_at": "2011-04-21T23:09:54Z"
},
{
"id": 7,
"author": {
"id": 44,
"name": "Cost spring our."
},
"name": "Because pattern.",
"created_at": "2010-01-04T08:21:29Z"
},
{
"id": 8,
"author": {
"id": 45,
"name": "Entire we certainly."
},
"name": "Program use feel.",
"created_at": "1972-11-30T15:49:50Z"
},
{
"id": 9,
"author": {
"id": 42,
"name": "Interest drop."
},
"name": "Purpose live might.",
"created_at": "1987-01-31T16:48:54Z"
},
{
"id": 10,
"author": {
"id": 12,
"name": "Sell data contain."
},
"name": "Everyone thing seem.",
"created_at": "2007-10-19T07:16:34Z"
}
],
"status": "success"
}
view raw response.json hosted with ❤ by GitHub

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:

from django.urls import reverse
from django_seed import Seed
from core.models import Author, Book
from rest_framework.test import APITestCase
seeder = Seed.seeder()
class BooksTestCase(APITestCase):
def test_list_books(self):
# Add dummy data to the Author and Book Table
seeder.add_entity(Author, 5)
seeder.add_entity(Book, 10)
seeder.execute()
# we expect the result in 1 query
with self.assertNumQueries(1):
response = self.client.get(reverse("book_list"), format="json")
# test output
$ ./manage.py test
.
.
.
AssertionError: 11 != 1 : 11 queries executed, 1 expected
Captured queries were:
1. SELECT "core_book"."id", "core_book"."name", "core_book"."author_id", "core_book"."created_at" FROM "core_book"
2. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 4 LIMIT 21
3. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 1 LIMIT 21
4. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 4 LIMIT 21
5. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 4 LIMIT 21
6. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 5 LIMIT 21
7. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 5 LIMIT 21
8. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 1 LIMIT 21
9. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 3 LIMIT 21
10. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 3 LIMIT 21
11. SELECT "core_author"."id", "core_author"."name" FROM "core_author" WHERE "core_author"."id" = 5 LIMIT 21
----------------------------------------------------------------------
Ran 1 test in 0.027s
FAILED (failures=1)
view raw tests.py hosted with ❤ by GitHub

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. 

class BookListCreateAPIView(generics.ListCreateAPIView):
serializer_class = BookSerializer
def get_queryset(self):
queryset = Book.objects.select_related("author").all()
return queryset
view raw views.py hosted with ❤ by GitHub

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

$ ./manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.024s
OK
Destroying test database for alias 'default'...
view raw test_output.sh hosted with ❤ by GitHub

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

>> queryset = Book.objects.select_related("author").all()
>> print(queryset.query)
SELECT "core_book"."id",
"core_book"."name",
"core_book"."author_id",
"core_book"."created_at",
"core_author"."id",
"core_author"."name"
FROM "core_book"
INNER JOIN "core_author" ON ("core_book"."author_id" = "core_author"."id")
view raw query.py hosted with ❤ by GitHub

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: 

{
"message": "",
"errors": [],
"data": [
{
"id": 1,
"author": {
"id": 3,
"name": "Meet teacher."
},
"name": "Body society.",
"created_at": "1973-08-03T02:43:22Z"
},
{
"id": 2,
"author": {
"id": 49,
"name": "Cause wait health."
},
"name": "Left next pretty.",
"created_at": "2000-07-07T03:37:10Z"
}
],
"status": "success"
}
view raw output.json hosted with ❤ by GitHub

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.

from rest_framework.renderers import JSONRenderer
from rest_framework.views import exception_handler
class CustomJSONRenderer(JSONRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
# reformat the response
response_data = {"message": "", "errors": [], "data": data, "status": "success"}
# call super to render the response
response = super(CustomJSONRenderer, self).render(
response_data, accepted_media_type, renderer_context
)
return response
view raw renderers.py hosted with ❤ by GitHub

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

REST_FRAMEWORK = {
"DEFAULT_RENDERER_CLASSES": (
"core.renderer.CustomJSONRenderer",
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
)
}
view raw settings.py hosted with ❤ by GitHub

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.

class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer()
book_display_name= serializers.SerializerMethodField(source="get_book_display_name")
def get_book_display_name(self, book):
return book.name.upper()
class Meta:
model = Book
fields = "__all__"
view raw serializer.py hosted with ❤ by GitHub

  • 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.

book_display_name = serializers.SerializerMethodField()

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.

class DynamicPaginationMixin(object):
"""
Controls pagination enable disable option using query param "pagination".
If pagination=false is passed in query params, data is returned without pagination
"""
def paginate_queryset(self, queryset):
pagination = self.request.query_params.get("pagination", "true")
if bool(pagination):
return None
return super().paginate_queryset(queryset)
view raw mixins.py hosted with ❤ by GitHub

# Remember to use mixin before the generics
class BookListCreateAPIView(DynamicPaginationMixin, generics.ListCreateAPIView):
serializer_class = BookSerializer
def get_queryset(self):
queryset = Book.objects.select_related("author").all()
return queryset
view raw views.py hosted with ❤ by GitHub

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

Did you like the blog? If yes, we're sure you'll also like to work with the people who write them - our best-in-class engineering team.

We're looking for talented developers who are passionate about new emerging technologies. If that's you, get in touch with us.

Explore current openings