Sitemap
Press enter or click to view image in full size

Django REST Framework (Backend): The Complete Guide

--

A practical guide for Python developers

Part 1: Mixins — Reusable Building Blocks

What Are Mixins?

Mixins are special classes that provide reusable functionality through multiple inheritance. Unlike regular classes, mixins aren’t meant to be instantiated alone — they add specific behaviors to other classes.

Why Django Uses Mixins

Without mixins, Django would need hundreds of view classes for every combination of CRUD operations. Mixins allow you to:

  • Reduce code duplication
  • Mix and match functionality like Lego blocks
  • Keep code maintainable
  • Test each piece independently

Example:

class ListCreateAPIView(ListModelMixin, CreateModelMixin, GenericAPIView):
pass # Combines LIST and CREATE in one view!

The 5 DRF Mixins

1. CreateModelMixin:

Purpose: Creates new objects via POST requests

class ArticleCreateView(mixins.CreateModelMixin, generics.GenericAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)

def perform_create(self, serializer):
serializer.save(author=self.request.user)

2. ListModelMixin

Purpose: Returns paginated lists of objects via GET request

class ArticleListView(mixins.ListModelMixin, generics.GenericAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
search_fields = ['title', 'content']

def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)

3. RetrieveModelMixin

Purpose: Fetches a single object by ID

class ArticleDetailView(mixins.RetrieveModelMixin, generics.GenericAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)

4. UpdateModelMixin

Purpose: Updates existing objects via PUT/PATCH

class ArticleUpdateView(mixins.UpdateModelMixin, generics.GenericAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)

def patch(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)

5. DestroyModelMixin

Purpose: Deletes objects via DELETE requests

class ArticleDeleteView(mixins.DestroyModelMixin, generics.GenericAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)

Important Rules

# ✅ CORRECT - Mixins first, then GenericAPIView
class MyView(CreateModelMixin, UpdateModelMixin, GenericAPIView):
pass
# ❌ WRONG - Missing queryset and serializer_class
class MyView(mixins.ListModelMixin, GenericAPIView):
pass # Will raise errors

Part 2: APIView — The Foundation

What is APIView?

APIView is the base class for all DRF views. It's Django's View class but designed specifically for REST APIs with built-in authentication, permissions, throttling, and content negotiation.

APIView Lifecycle

HTTP Request
↓
[URL Router]
↓
[as_view()]
↓
[dispatch()]
↓
[initial()]
├─ perform_authentication() → request.user
├─ check_permissions()
├─ check_throttles()
↓
[HTTP Method Handler] → get(), post(), put(), etc.
↓
[Response]

Basic Example

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
class ArticleAPIView(APIView):
permission_classes = [IsAuthenticated]

def get(self, request):
articles = Article.objects.filter(author=request.user)
return Response(ArticleSerializer(articles, many=True).data)

def post(self, request):
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
serializer.save(author=request.user)
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)

Authentication vs Permissions

Authentication = Who are you?
Permissions = What can you do?

class IsCreatorPermission(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.created_by == request.user
class MyView(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated, IsCreatorPermission]

Request Object

# Accessing data
request.data # Parsed request body (JSON, form, etc.)
request.query_params # Query string parameters
request.user # Authenticated user
request.auth # Authentication context

HTTP Methods

class ArticleAPIView(APIView):
def get(self, request, pk=None): # Read
pass

def post(self, request): # Create
pass

def put(self, request, pk): # Update (full)
pass

def patch(self, request, pk): # Update (partial)
pass

def delete(self, request, pk): # Delete
pass

When to Use APIView

  • When you need full control
  • For complex business logic
  • When you don’t follow REST conventions
  • For custom behavior that doesn’t fit generic views

Part 3: GenericAPIView — The Bridge

What is GenericAPIView?

GenericAPIView extends APIView and adds model-specific functionality. It's the bridge between APIView and Mixins.

Core Attributes

class ArticleView(GenericAPIView):
queryset = Article.objects.all() # Base queryset
serializer_class = ArticleSerializer # Serializer
lookup_field = 'slug' # Field for lookups
pagination_class = PageNumberPagination # Pagination
filter_backends = [DjangoFilterBackend] # Filtering

Core Methods

class ArticleView(GenericAPIView):
def get_queryset(self):
"""Dynamic queryset based on user"""
if self.request.user.is_staff:
return Article.objects.all()
return Article.objects.filter(author=self.request.user)

def get_serializer_class(self):
"""Different serializers for different actions"""
if self.request.method == 'GET':
return ArticleListSerializer
return ArticleCreateSerializer

def get_serializer_context(self):
"""Add extra context to serializer"""
context = super().get_serializer_context()
context['user'] = self.request.user
return context

def get_object(self):
"""Override to add custom object logic"""
obj = super().get_object()
obj.views_count += 1
obj.save()
return obj

Why Use GenericAPIView?

Without:

class ArticleView(APIView):
def get(self, request):
articles = Article.objects.all()
return Response(ArticleSerializer(articles, many=True).data)

With:

class ArticleView(GenericAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

APIView vs GenericAPIView

FeatureAPIViewGenericAPIViewQuerysetManualBuilt-inSerializersManualBuilt-inObject lookupManualBuilt-inFilteringManualBuilt-inPaginationManualBuilt-in

Part 4: Generic Views — Ready-to-Use

Examples

# Simple list view
class ArticleListAPIView(generics.ListAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# List and create
class ArticleListCreateAPIView(generics.ListCreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = [IsAuthenticatedOrReadOnly]

def perform_create(self, serializer):
serializer.save(author=self.request.user)
# Full CRUD
class ArticleRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = [IsAuthenticated, IsAuthorOrAdmin]

Best Practices

  1. Choose the simplest view for your needs
  2. Use perform_create()/perform_update() for custom logic
  3. Always optimize querysets with select_related
  4. Set proper permission_classes

Part 5: ViewSets — Grouping Views

What are ViewSets?

ViewSets group related views into a single class. Instead of separate classes for list, create, retrieve, update, and delete, you write one class.

Types of ViewSets

1. ViewSet (Custom)

class ArticleViewSet(viewsets.ViewSet):
def list(self, request):
articles = Article.objects.all()
return Response(ArticleSerializer(articles, many=True).data)

def create(self, request):
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)

2. ModelViewSet (Full CRUD)

class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = [IsAuthenticated]

def perform_create(self, serializer):
serializer.save(author=self.request.user)

3. ReadOnlyModelViewSet

class ArticleViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Article.objects.filter(status='published')
serializer_class = ArticleSerializer

Using Routers

# urls.py
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'articles', ArticleViewSet, basename='article')
urlpatterns = router.urls# Generated URLs:
# GET /articles/ - list()
# POST /articles/ - create()
# GET /articles/{id}/ - retrieve()
# PUT /articles/{id}/ - update()
# PATCH /articles/{id}/ - partial_update()
# DELETE /articles/{id}/ - destroy()

Best Practices

  1. Use ModelViewSet for standard CRUD
  2. Override perform_create()/perform_update() for custom logic
  3. Use different serializers for different actions
  4. Always set proper permission_classes

Part 6: Custom Actions with @action

Introduction

The @action decorator adds custom endpoints to ViewSets beyond standard CRUD operations.

Parameters

@action(
detail=True, # True for single object, False for collection
methods=['post'], # HTTP methods
url_path='publish', # URL path
url_name='publish', # URL name
permission_classes=[IsAdminUser] # Override permissions
)

Detail vs Collection Actions

Detail Actions (detail=True):

  • Operate on a single object
  • URL: /articles/{id}/publish/
  • Receive pk parameter

Collection Actions (detail=False):

  • Operate on entire collection
  • URL: /articles/trending/
  • No pk parameter

Real Examples

1. Publish Article (Detail Action)

class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
article = self.get_object()
article.status = 'published'
article.published_at = timezone.now()
article.save()
return Response({"status": "published"})

2. Like Article (Detail Action)

@action(detail=True, methods=['post'])
def like(self, request, pk=None):
article = self.get_object()
article.likes_count += 1
article.save(update_fields=['likes_count'])
return Response({'likes': article.likes_count})

3. Trending Articles (Collection Action)

@action(detail=False, methods=['get'])
def trending(self, request):
articles = Article.objects.filter(
status='published'
).order_by('-views_count', '-likes_count')[:10]
serializer = self.get_serializer(articles, many=True)
return Response(serializer.data)

4. Approve/Reject (Admin Only)

@action(detail=True, methods=['post'], permission_classes=[IsAdminUser])
def approve(self, request, pk=None):
article = self.get_object()
article.status = 'approved'
article.save()
return Response({"status": "approved"})

@action(detail=True, methods=['post'], permission_classes=[IsAdminUser])
def reject(self, request, pk=None):
article = self.get_object()
article.status = 'rejected'
article.save()
return Response({"status": "rejected"})

When to Use @action vs Separate View

Use @action when:

  • The action is related to an existing resource
  • It’s a simple operation on the resource
  • You want to keep related code together

Use Separate View when:

  • The functionality is completely different
  • You need complex logic
  • It doesn’t fit the resource pattern

Part 7: Serializer Basics

What is a Serializer?

A serializer converts complex data types (Django models) into Python datatypes that can be rendered into JSON. It also provides deserialization — converting incoming data back to Python objects.

Basic Serializer

class ArticleSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(max_length=200)
content = serializers.CharField()
author = serializers.CharField(max_length=100)
created_at = serializers.DateTimeField(read_only=True)

def create(self, validated_data):
return Article.objects.create(**validated_data)

def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
instance.content = validated_data.get('content', instance.content)
instance.save()
return instance

Key Attributes

serializer = ArticleSerializer(data=request.data)
serializer.initial_data    # Raw input data
serializer.validated_data # Cleaned data after validation
serializer.data # Serialized output
serializer.errors # Validation errors
serializer.instance # Model instance

Using Serializers in Views

class ArticleCreateView(APIView):
def post(self, request):
serializer = ArticleSerializer(data=request.data)

if serializer.is_valid():
article = serializer.save(author=request.user)
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)

Part 8: ModelSerializer

What is ModelSerializer?

ModelSerializer automatically creates a Serializer from a model, including fields, validators, and create()/update() methods.

Basic Example

class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__' # All fields
# Or: fields = ['id', 'title', 'content']
# Or: exclude = ['updated_at']

Meta Options

class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['id', 'title', 'content', 'author', 'status']
read_only_fields = ['id', 'created_at', 'author']
extra_kwargs = {
'title': {'max_length': 200, 'required': True},
'content': {'style': {'base_template': 'textarea.html'}}
}
depth = 1 # Nested representation depth

Custom Validation

class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'

def validate_title(self, value):
if 'spam' in value.lower():
raise serializers.ValidationError("Title contains spam")
return value

def validate(self, data):
if data['title'] == data['content']:
raise serializers.ValidationError("Title and content must differ")
return data

Overriding create/update

class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'

def create(self, validated_data):
validated_data['author'] = self.context['request'].user
return super().create(validated_data)

Part 9: Serializer Lifecycle

Complete Flow

Client Request (JSON)
↓
serializer = ArticleSerializer(data=request.data)
↓
is_valid()
├── Field conversion (to_internal_value)
├── Field validators
├── validate_<field>() methods
└── validate() method
↓
validated_data (cleaned data)
↓
save()
├── create() (if no instance)
└── update() (if instance provided)
↓
Model Instance
↓
to_representation()
↓
serializer.data (output dict)
↓
Response (JSON)

Validation Stages

class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['title', 'content']

# Stage 1: Field conversion (automatic)
# "2024-01-01" → datetime.date(2024, 1, 1)

# Stage 2: Field validators (automatic from model)
# max_length, min_length, unique, choices

# Stage 3: Field-specific validation
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError("Title too short")
return value

# Stage 4: Object-level validation
def validate(self, data):
if data['title'] == data['content']:
raise serializers.ValidationError("Title and content must differ")
return data

Error Handling

# With manual checking
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)
# With automatic exception
serializer = ArticleSerializer(data=request.data)
serializer.is_valid(raise_exception=True) # Returns 400 automatically
serializer.save()
# Error format
# {
# "title": ["This field is required."],
# "content": ["Content too short."]
# }

Key Methods

class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'

def to_internal_value(self, data):
"""Convert input data to Python types."""
# Normalize data before validation
data = data.copy()
if 'title' in data:
data['title'] = data['title'].strip().capitalize()
return super().to_internal_value(data)

def to_representation(self, instance):
"""Format output data for response."""
data = super().to_representation(instance)
# Add computed fields
data['word_count'] = len(instance.content.split())
# Format dates
data['created_at'] = data['created_at'].strftime('%B %d, %Y')
return data

Part 10: Validation In-Depth

Types of Validation

1. Field-Level Validation

def validate_title(self, value):
if 'spam' in value.lower():
raise serializers.ValidationError("Spam detected")
if len(value) < 5:
raise serializers.ValidationError("Title too short")
return value

2. Object-Level Validation

def validate(self, data):
if data['start_date'] > data['end_date']:
raise serializers.ValidationError("Start date must be before end date")
return data

3. Custom Validators

from rest_framework import serializers
def validate_no_spam(value):
if 'spam' in value.lower():
raise serializers.ValidationError("Contains spam")
return value
class ArticleSerializer(serializers.ModelSerializer):
title = serializers.CharField(validators=[validate_no_spam])

4. Built-in Validators

from rest_framework.validators import UniqueValidator
from django.core.validators import MinLengthValidator
class ArticleSerializer(serializers.ModelSerializer):
title = serializers.CharField(
validators=[
MinLengthValidator(5),
UniqueValidator(queryset=Article.objects.all())
]
)

Execution Order

1. Required field validation
2. Field type conversion
3. Field validators (built-in)
4. Custom field validators
5. validate_<field>() methods
6. Object-level validate()

Production Example

class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ['items', 'total', 'discount_code']

def validate_items(self, value):
if not value:
raise serializers.ValidationError("Need at least one item")
return value

def validate_discount_code(self, value):
if value and not Discount.objects.filter(code=value).exists():
raise serializers.ValidationError("Invalid discount code")
return value

def validate(self, data):
if data.get('discount_code'):
discount = Discount.objects.get(code=data['discount_code'])
if data['total'] < discount.min_order:
raise serializers.ValidationError("Minimum order required")
return data

Part 11: create() and update()

How save() Works

# Create flow
serializer = ArticleSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
article = serializer.save() # Calls create()
# Update flow
article = Article.objects.get(pk=1)
serializer = ArticleSerializer(article, data=request.data)
serializer.is_valid(raise_exception=True)
article = serializer.save() # Calls update()

Custom create()

class OrderSerializer(serializers.ModelSerializer):
items = OrderItemSerializer(many=True)

class Meta:
model = Order
fields = ['id', 'user', 'items', 'total']
read_only_fields = ['id', 'user', 'total']

def create(self, validated_data):
items_data = validated_data.pop('items')
total = sum(item['price'] * item['quantity'] for item in items_data)
validated_data['total'] = total
order = Order.objects.create(**validated_data)
for item_data in items_data:
OrderItem.objects.create(order=order, **item_data)
return order

Custom update()

def update(self, instance, validated_data):
items_data = validated_data.pop('items', None)

# Update order fields
for attr, value in validated_data.items():
setattr(instance, attr, value)

# Update items if provided
if items_data is not None:
instance.items.all().delete()
for item_data in items_data:
OrderItem.objects.create(order=instance, **item_data)
total = sum(item['price'] * item['quantity'] for item in items_data)
instance.total = total

instance.save()
return instance

perform_create() in Views

# In Generic View
class ArticleCreateView(generics.CreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

def perform_create(self, serializer):
# Called after validation, before save
serializer.save(author=self.request.user)

create() vs perform_create()

MethodLocationPurposecreate()SerializerModel logic, nested objectsperform_create()ViewRequest logic (author, user)

Part 12: to_internal_value()

Purpose

Converts raw input data into Python types for validation and saving.

# Input: {"title": "  hello  ", "age": "25"}
# Output: {"title": "hello", "age": 25}

Custom Usage

class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['title', 'content', 'tags']

def to_internal_value(self, data):
data = data.copy()

# Normalize text
if 'title' in data:
data['title'] = data['title'].strip().capitalize()

# Convert string to list
if 'tags' in data and isinstance(data['tags'], str):
data['tags'] = [tag.strip() for tag in data['tags'].split(',')]

return super().to_internal_value(data)

vs to_representation()

MethodDirectionPurposeto_internal_value()Input → PythonConvert raw inputto_representation()Python → OutputFormat for response

Part 13: to_representation()

Purpose

Formats model instances into a dictionary for API responses.

Custom Usage

class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['id', 'title', 'content', 'author', 'created_at']

def to_representation(self, instance):
data = super().to_representation(instance)

# Format dates
data['created_at'] = data['created_at'].strftime('%B %d, %Y')

# Add computed fields
data['word_count'] = len(instance.content.split())
data['reading_time'] = max(1, len(instance.content.split()) // 200)

# Conditional fields based on user
request = self.context.get('request')
if request and request.user == instance.author:
data['can_edit'] = True

# Format related objects
if instance.author:
data['author'] = {
'id': instance.author.id,
'name': instance.author.get_full_name()
}

return data

Common Use Cases

def to_representation(self, instance):
data = super().to_representation(instance)

# 1. Date formatting
data['created_at'] = data['created_at'].strftime('%Y-%m-%d %H:%M:%S')

# 2. Add computed fields
data['summary'] = instance.content[:200] + '...'

# 3. Conditional inclusion
if self.context.get('detailed'):
data['full_content'] = instance.content

# 4. Format URLs
if instance.image:
data['image'] = {
'url': instance.image.url,
'thumbnail': instance.image.thumbnail.url
}

# 5. Remove sensitive data
if not self.context.get('is_admin'):
data.pop('admin_notes', None)

return data

Part 14: Nested Serializers

Basic Nested Serializer

class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = ['id', 'name', 'bio']
class ArticleSerializer(serializers.ModelSerializer):
author = AuthorSerializer() # Nested

class Meta:
model = Article
fields = ['id', 'title', 'content', 'author']

Read-Only Nested

class ArticleSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)

class Meta:
model = Article
fields = ['id', 'title', 'content', 'author', 'tags']

Writable Nested (create/update)

class ArticleSerializer(serializers.ModelSerializer):
author = AuthorSerializer()
tags = TagSerializer(many=True)

class Meta:
model = Article
fields = ['id', 'title', 'content', 'author', 'tags']

def create(self, validated_data):
author_data = validated_data.pop('author')
tags_data = validated_data.pop('tags')

# Create or get author
author, _ = Author.objects.get_or_create(**author_data)
article = Article.objects.create(author=author, **validated_data)

# Add tags
for tag_data in tags_data:
tag, _ = Tag.objects.get_or_create(**tag_data)
article.tags.add(tag)

return article

Reverse Relationship

class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['id', 'title', 'content']
class AuthorSerializer(serializers.ModelSerializer):
articles = ArticleSerializer(many=True, read_only=True)

class Meta:
model = Author
fields = ['id', 'name', 'bio', 'articles']

Using depth

class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'
depth = 1 # Automatically nested one level

Performance Note

# Optimize nested serializers to avoid N+1 queries
class ArticleViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Article.objects.select_related('author', 'category').prefetch_related('tags')

Part 15: Query Optimization

The N+1 Problem

# PROBLEM: N+1 queries
articles = Article.objects.all()
for article in articles:
print(article.author.name) # Queries author for each article
# SOLUTION: select_related
articles = Article.objects.select_related('author').all()

select_related() (Foreign Keys)

# One query with JOIN
articles = Article.objects.select_related('author', 'category').all()
# Deep relationships
articles = Article.objects.select_related('author__profile').all()

prefetch_related() (ManyToMany, Reverse)

# Two queries (articles + tags)
articles = Article.objects.prefetch_related('tags').all()
# Multiple relationships
articles = Article.objects.prefetch_related('tags', 'comments').all()
# With filtering
from django.db.models import Prefetch
articles = Article.objects.prefetch_related(
Prefetch('comments', queryset=Comment.objects.filter(approved=True))
).all()

Advanced Optimization

from django.db.models import Count, Q
# Annotate with counts
articles = Article.objects.annotate(
comment_count=Count('comments'),
like_count=Count('likes')
)
# Conditional annotation
articles = Article.objects.annotate(
recent_comments=Count(
'comments',
filter=Q(comments__created_at__gte=timezone.now() - timedelta(days=7))
)
)
# Only specific fields
articles = Article.objects.only('id', 'title', 'author__username')
# Bulk operations
Article.objects.filter(status='draft').update(status='published')

Best Practices

  1. Always use select_related() for ForeignKey fields
  2. Use prefetch_related() for ManyToMany and reverse relationships
  3. Use only() and defer() for large models
  4. Use exists() instead of count() > 0
  5. Use bulk_create() and bulk_update() for mass operations
  6. Monitor queries with django-debug-toolbar

Part 16: QuerySets — Complete Guide

What is a QuerySet?

A QuerySet represents a collection of database queries. It’s lazy — it doesn’t execute until evaluated.

Lazy Evaluation

# Not executed yet
articles = Article.objects.filter(status='published')
# Still not executed
articles = articles.filter(created_at__gte=timezone.now() - timedelta(days=7))
# Executes here
for article in articles:
print(article.title)
# Or here
first_article = articles.first()
count = articles.count()

Basic Methods

# Get all
articles = Article.objects.all()
# Filter
articles = Article.objects.filter(status='published', author=user)
articles = Article.objects.filter(title__icontains='python')
# Exclude
articles = Article.objects.exclude(status='draft')
# Get single
article = Article.objects.get(id=1) # Raises DoesNotExist if not found
# First/Last
first = Article.objects.first()
last = Article.objects.last()
# Check if exists
if Article.objects.filter(status='published').exists():
print("Has published articles")
# Count
count = Article.objects.filter(status='published').count()

Field Lookups

# Exact
Article.objects.filter(title__exact='My Article')
Article.objects.filter(title='My Article') # Shortcut
# Contains
Article.objects.filter(title__contains='Python')
Article.objects.filter(title__icontains='python') # Case-insensitive
# In list
Article.objects.filter(status__in=['published', 'archived'])
# Greater/Less than
Article.objects.filter(views_count__gt=100)
Article.objects.filter(created_at__gte=timezone.now() - timedelta(days=7))
# Date lookups
Article.objects.filter(created_at__year=2024)
Article.objects.filter(created_at__month=1)
Article.objects.filter(published_at__isnull=True)

Values Extraction

# Dictionaries
articles = Article.objects.values('id', 'title', 'author__username')
# [{'id': 1, 'title': 'Article 1', 'author__username': 'john'}]
# Tuples
articles = Article.objects.values_list('id', 'title')
# [(1, 'Article 1'), (2, 'Article 2')]
# Flat list
ids = Article.objects.values_list('id', flat=True)
# [1, 2, 3]
# Only specific fields (lazy loading)
articles = Article.objects.only('id', 'title')
# Defer heavy fields
articles = Article.objects.defer('content', 'metadata')

Ordering

# Ascending
articles = Article.objects.order_by('created_at')
# Descending
articles = Article.objects.order_by('-created_at')
# Multiple fields
articles = Article.objects.order_by('-created_at', 'title')
# Random
articles = Article.objects.order_by('?')

Aggregation & Annotation

from django.db.models import Count, Sum, Avg, Max, Min
# Aggregation (returns dictionary)
stats = Article.objects.aggregate(
total=Sum('views_count'),
avg=Avg('views_count'),
max=Max('views_count')
)
# Annotation (adds computed field to each object)
articles = Article.objects.annotate(
comment_count=Count('comments'),
like_count=Count('likes')
)

F Expressions

from django.db.models import F
# Update using current value
Article.objects.filter(id=1).update(views_count=F('views_count') + 1)
# Compare fields
articles = Article.objects.filter(views_count__gt=F('likes_count') * 2)

Q Objects

from django.db.models import Q
# OR conditions
articles = Article.objects.filter(
Q(status='published') | Q(status='featured')
)
# NOT conditions
articles = Article.objects.filter(~Q(status='draft'))
# Complex combinations
articles = Article.objects.filter(
Q(status='published') &
(Q(title__icontains='python') | Q(content__icontains='python'))
)

Case/When

from django.db.models import Case, When, Value, CharField
articles = Article.objects.annotate(
popularity=Case(
When(views_count__gte=1000, then=Value('high')),
When(views_count__gte=100, then=Value('medium')),
default=Value('low'),
output_field=CharField()
)
)

Subquery

from django.db.models import Subquery, OuterRef, Exists
# Subquery
recent_articles = Article.objects.filter(
author=OuterRef('id')
).order_by('-created_at')[:1]
authors = User.objects.annotate(
latest_article_title=Subquery(recent_articles.values('title'))
)
# Exists
has_published = Article.objects.filter(
author=OuterRef('id'),
status='published'
)
authors = User.objects.annotate(
has_published=Exists(has_published)
)

Bulk Operations

# Bulk create
articles = [
Article(title=f'Article {i}', content=f'Content {i}')
for i in range(100)
]
Article.objects.bulk_create(articles)
# Bulk update
articles = Article.objects.filter(status='published')
for article in articles:
article.status = 'archived'
Article.objects.bulk_update(articles, ['status'])
# Update without retrieving
Article.objects.filter(status='draft').update(status='published')
# Delete
Article.objects.filter(status='deleted').delete()

Transactions

from django.db import transaction
@transaction.atomic
def process_article(article_id):
# All operations in this block are atomic
article = Article.objects.select_for_update().get(id=article_id)
article.status = 'processing'
article.save()
# ... more operations
article.status = 'completed'
article.save()

Performance Best Practices

  1. Use select_related() for ForeignKey fields
  2. Use prefetch_related() for ManyToMany fields
  3. Use only() and defer() for large models
  4. Use exists() instead of count() > 0
  5. Use pagination for large result sets
  6. Use bulk operations for mass updates
  7. Use indexes on frequently filtered fields
  8. Monitor queries with django-debug-toolbar
  9. Avoid filtering in Python — filter in database
  10. Use transactions for atomic operations

Common Mistakes

# ❌ N+1 queries
articles = Article.objects.all()
for article in articles:
print(article.author.name)
# ✅ Solution
articles = Article.objects.select_related('author').all()
# ❌ Filtering in Python
articles = Article.objects.all()
published = [a for a in articles if a.status == 'published']
# ✅ Solution
articles = Article.objects.filter(status='published')
# ❌ Using count() for existence
if Article.objects.filter(status='published').count() > 0:
pass
# ✅ Solution
if Article.objects.filter(status='published').exists():
pass
# ❌ All without pagination
articles = Article.objects.all() # Loads everything
# ✅ Solution
articles = Article.objects.all()[:100]

Final Summary

You’ve now covered the complete Django REST Framework ecosystem:

  • Mixins — Reusable CRUD building blocks
  • APIView — Foundation for all DRF views
  • GenericAPIView — Bridge to model-based views
  • Generic Views — Ready-to-use CRUD views
  • ViewSets — Grouping related views
  • Custom Actions — Extending ViewSets
  • Serializers — Data conversion and validation
  • ModelSerializer — Automated serialization
  • Serializer Lifecycle — Complete data flow
  • Validation — Data integrity and business rules
  • create()/update() — Saving data
  • to_internal_value() — Input processing
  • to_representation() — Output formatting
  • Nested Serializers — Handling relationships
  • Query Optimization — Performance tuning
  • QuerySets — Database interaction

With this knowledge, you can build production-ready REST APIs that are efficient, maintainable, and scalable.

Get Shahab Uddin’s stories in your inbox

Join Medium for free to get updates from this writer.

Key Takeaways:

  • Always optimize your queries with select_related/prefetch_related
  • Validate data thoroughly at multiple levels
  • Use appropriate views (APIView, GenericAPIView, ViewSets) based on needs
  • Keep serializers clean with proper separation of concerns
  • Monitor performance and database queries

Happy Coding! 🚀

--

--