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 errorsPart 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 IsAuthenticatedclass 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.userclass 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 contextHTTP 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
passWhen 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] # FilteringCore 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 objWhy 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 = ArticleSerializerAPIView 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
- Choose the simplest view for your needs
- Use
perform_create()/perform_update()for custom logic - Always optimize querysets with
select_related - 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 = ArticleSerializerUsing Routers
# urls.py
from rest_framework.routers import DefaultRouterrouter = 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
- Use
ModelViewSetfor standard CRUD - Override
perform_create()/perform_update()for custom logic - Use different serializers for different actions
- 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
pkparameter
Collection Actions (detail=False):
- Operate on entire collection
- URL:
/articles/trending/ - No
pkparameter
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 instanceKey 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 instanceUsing 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 depthCustom 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 dataOverriding 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 dataError 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 dataPart 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 value2. 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 data3. Custom Validators
from rest_framework import serializersdef validate_no_spam(value):
if 'spam' in value.lower():
raise serializers.ValidationError("Contains spam")
return valueclass 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 MinLengthValidatorclass 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 dataPart 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 orderCustom 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 instanceperform_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 dataCommon 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 dataPart 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 articleReverse 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 levelPerformance 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
- Always use
select_related()for ForeignKey fields - Use
prefetch_related()for ManyToMany and reverse relationships - Use
only()anddefer()for large models - Use
exists()instead ofcount() > 0 - Use
bulk_create()andbulk_update()for mass operations - 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, CharFieldarticles = 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
- Use select_related() for ForeignKey fields
- Use prefetch_related() for ManyToMany fields
- Use only() and defer() for large models
- Use exists() instead of count() > 0
- Use pagination for large result sets
- Use bulk operations for mass updates
- Use indexes on frequently filtered fields
- Monitor queries with django-debug-toolbar
- Avoid filtering in Python — filter in database
- 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! 🚀





