Django REST Framework — 150 Deep Questions & Answers
Who is this for? Developers who want to understand *why* DRF works the way it does — not just memorize APIs, but build a mental model that makes every decision obvious.
Django REST Framework — 150 Deep Questions & Answers
**Who is this for?** Developers who want to understand *why* DRF works the way it does — not just memorize APIs, but build a mental model that makes every decision obvious.
🔰 Basics & Setup
1. What is Django REST Framework (DRF) and how does it differ from plain Django?
Django handles HTML responses — it was built for server-rendered web pages. DRF sits on top of Django and adds everything you need to build APIs: serialization, authentication, permissions, throttling, and content negotiation.
Plain Django can return JSON, but you'd write everything manually:
# Plain Django — manual, verbose
import json
from django.http import HttpResponse
def user_list(request):
users = list(User.objects.values('id', 'username', 'email'))
return HttpResponse(json.dumps(users), content_type='application/json')With DRF, the same thing becomes:
# DRF — structured, powerful
from rest_framework import generics
from .serializers import UserSerializer
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializerDRF gives you: validation, serialization, authentication, browsable API, pagination, filtering — all wired together.
2. How do you install and configure DRF in a Django project?
pip install djangorestframework# settings.py
INSTALLED_APPS = [
...
'rest_framework',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}# urls.py — add browsable API login
urlpatterns = [
...
path('api-auth/', include('rest_framework.urls')),
]3. What is the role of `DEFAULT_RENDERER_CLASSES` in DRF settings?
Renderers decide how your response data gets formatted before it's sent to the client. By default DRF includes JSONRenderer (returns JSON) and BrowsableAPIRenderer (returns the interactive HTML browser).
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer', # API clients get JSON
'rest_framework.renderers.BrowsableAPIRenderer', # Browsers get HTML UI
]
}In production you often remove BrowsableAPIRenderer to reduce overhead and hide your API structure:
# Production settings
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
]
}4. What does the `REST_FRAMEWORK` settings dict control?
It's the global configuration for all DRF behavior. Think of it as the "default behavior" for every view unless you override at the view level.
REST_FRAMEWORK = {
# Who can access by default
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'],
# How they prove identity
'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.TokenAuthentication'],
# How many requests per second
'DEFAULT_THROTTLE_CLASSES': ['rest_framework.throttling.UserRateThrottle'],
'DEFAULT_THROTTLE_RATES': {'user': '1000/day'},
# How results are split into pages
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
# How filtering works
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
# What formats are accepted/returned
'DEFAULT_RENDERER_CLASSES': ['rest_framework.renderers.JSONRenderer'],
'DEFAULT_PARSER_CLASSES': ['rest_framework.parsers.JSONParser'],
}5. What is an `APIView` and how does it differ from Django's `View`?
Django's View dispatches HTTP methods but knows nothing about APIs. APIView wraps it and adds:
- request.data instead of request.POST - Content negotiation (JSON in, JSON out) - Authentication and permission checking on every request - Throttling - Better exception handling (returns JSON errors, not HTML 500 pages)
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
class ProductView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, pk):
product = get_object_or_404(Product, pk=pk)
serializer = ProductSerializer(product)
return Response(serializer.data)
def put(self, request, pk):
product = get_object_or_404(Product, pk=pk)
serializer = ProductSerializer(product, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)6. How does DRF handle content negotiation?
DRF reads the Accept header from the request and picks the best renderer. It reads Content-Type to pick the right parser for incoming data.
# Client sends: Accept: application/json
# DRF uses: JSONRenderer
# Client sends: Accept: text/html
# DRF uses: BrowsableAPIRenderer
# Per-view override
class MyView(APIView):
renderer_classes = [JSONRenderer] # Force JSON only for this view7. What is the browsable API and how do you disable it?
The browsable API is DRF's built-in HTML interface — it lets you explore, make requests, and see responses in a browser. Great for development, but exposes your API structure in production.
# Disable globally in production
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
# BrowsableAPIRenderer removed
]
}
# Or conditionally based on environment
import os
renderers = ['rest_framework.renderers.JSONRenderer']
if os.environ.get('DJANGO_ENV') == 'development':
renderers.append('rest_framework.renderers.BrowsableAPIRenderer')
REST_FRAMEWORK = {'DEFAULT_RENDERER_CLASSES': renderers}8. What is the difference between `api_view` decorator and `APIView` class?
api_view is for function-based views. APIView is for class-based views. Same power, different style.
# Function-based with @api_view
from rest_framework.decorators import api_view
@api_view(['GET', 'POST'])
def product_list(request):
if request.method == 'GET':
products = Product.objects.all()
return Response(ProductSerializer(products, many=True).data)
elif request.method == 'POST':
serializer = ProductSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)Use @api_view for simple, one-off endpoints. Use APIView or generics when you need inheritance and reuse.
9. How do you set global default settings vs per-view settings in DRF?
Global = REST_FRAMEWORK dict in settings.py. Per-view = class attributes on the view.
# Global (settings.py)
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'],
}
# Per-view override
class PublicProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [AllowAny] # Override global auth
authentication_classes = [] # No auth needed
throttle_classes = [AnonRateThrottle] # Different throttleThe per-view setting always wins.
10. What is `DEFAULT_PARSER_CLASSES` and which parsers are available?
Parsers decode incoming request bodies. DRF picks the right one based on the request's Content-Type header.
REST_FRAMEWORK = {
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser', # Content-Type: application/json
'rest_framework.parsers.FormParser', # Content-Type: application/x-www-form-urlencoded
'rest_framework.parsers.MultiPartParser', # Content-Type: multipart/form-data (file uploads)
]
}
# Per-view (e.g. file upload endpoint only)
class FileUploadView(APIView):
parser_classes = [MultiPartParser, FormParser]
def post(self, request):
file = request.FILES['file']
# handle file
return Response({'filename': file.name})11. How do you return a JSON response from a DRF view?
Use Response() — never HttpResponse with json.dumps(). DRF's Response handles content negotiation automatically.
from rest_framework.response import Response
from rest_framework import status
class OrderView(APIView):
def get(self, request):
data = {'order_id': 123, 'status': 'pending'}
return Response(data) # 200 by default
def post(self, request):
# Created
return Response({'id': 1}, status=status.HTTP_201_CREATED)
def delete(self, request, pk):
# No content
return Response(status=status.HTTP_204_NO_CONTENT)12. What HTTP methods does `APIView` support out of the box?
APIView dispatches to methods named after HTTP verbs: get, post, put, patch, delete, head, options, trace. You only define the ones you want.
class ArticleView(APIView):
def get(self, request, pk): # Handles GET
...
def patch(self, request, pk): # Handles PATCH
...
# POST not defined → 405 Method Not Allowed13. How does DRF's request object differ from Django's `HttpRequest`?
DRF wraps Django's request and enhances it:
# Django's request
request.POST # Only form data
request.FILES # File uploads
request.META # Headers (raw)
# DRF's request (wraps the above)
request.data # ANY parsed data: JSON, form, multipart — one unified interface
request.query_params # Same as request.GET but clearer name
request.user # Authenticated user (after auth classes run)
request.auth # Auth token/credential object
request.accepted_renderer # Which renderer was selectedclass MyView(APIView):
def post(self, request):
# Works for JSON body, form data, multipart — same code
name = request.data.get('name')
page = request.query_params.get('page', 1)14. What is `request.data` and how does it differ from `request.POST`?
request.POST only works for form-encoded bodies. request.data works for JSON, form, XML — whatever parser is configured.
# Client sends JSON: {"username": "alok", "password": "secret"}
# request.POST → empty {}
# request.data → {"username": "alok", "password": "secret"} ✓
class LoginView(APIView):
def post(self, request):
username = request.data.get('username') # Works for all content types
password = request.data.get('password')15. How do you enable CORS in a DRF project?
pip install django-cors-headers# settings.py
INSTALLED_APPS = [..., 'corsheaders']
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # Must be FIRST
'django.middleware.common.CommonMiddleware',
...
]
# Allow specific origins (production)
CORS_ALLOWED_ORIGINS = [
'https://app.myapp.de',
'https://app.myapp.at',
]
# Allow all origins (development only)
CORS_ALLOW_ALL_ORIGINS = True # Never use in production
# Allow credentials (cookies/auth headers)
CORS_ALLOW_CREDENTIALS = True
# Custom headers
CORS_ALLOW_HEADERS = [
'authorization',
'content-type',
'x-requested-with',
]📦 Serializers
16. What is a serializer in DRF and what problem does it solve?
A serializer does two jobs: 1. Outgoing: Converts Python objects (Django models) → JSON 2. Incoming: Validates and converts JSON → Python objects (ready to save)
Without serializers you'd write validation and conversion by hand for every endpoint. Serializers give you a schema-based, reusable, testable layer.
from rest_framework import serializers
class PropertySerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(max_length=200)
price = serializers.DecimalField(max_digits=10, decimal_places=2)
is_active = serializers.BooleanField(default=True)17. What is the difference between `Serializer` and `ModelSerializer`?
Serializer is fully manual — you define every field. ModelSerializer introspects your Django model and generates fields automatically.
# Manual Serializer — you define everything
class PropertySerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(max_length=200)
price = serializers.DecimalField(max_digits=10, decimal_places=2)
def create(self, validated_data):
return Property.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
instance.save()
return instance
# ModelSerializer — auto-generates fields from model
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = ['id', 'title', 'price']
# create() and update() are auto-implemented!Use ModelSerializer by default. Use Serializer when you need full control or there's no model involved.
18. How does `ModelSerializer` automatically generate fields?
DRF maps Django model field types to serializer field types:
| Model Field | Serializer Field | |-------------|-----------------| | CharField | CharField | | IntegerField | IntegerField | | DecimalField | DecimalField | | BooleanField | BooleanField | | ForeignKey | PrimaryKeyRelatedField | | DateTimeField | DateTimeField |
class Property(models.Model):
title = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = '__all__'
# Auto-generates: title (CharField), price (DecimalField),
# owner (PrimaryKeyRelatedField), created_at (DateTimeField, read_only)19. What is the `Meta` class inside a serializer?
It's the configuration for ModelSerializer. Tells DRF which model to use and which fields to include.
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property # Which model
fields = ['id', 'title', 'price'] # Explicit field list (recommended)
# OR
fields = '__all__' # All fields (be careful — exposes everything)
# OR
exclude = ['internal_notes'] # All except these
read_only_fields = ['id', 'created_at']
extra_kwargs = {
'price': {'required': True, 'min_value': 0},
}20. How do you make a field read-only in a serializer?
Three ways:
class PropertySerializer(serializers.ModelSerializer):
# Method 1: field-level
slug = serializers.SlugField(read_only=True)
# Method 2: Meta.read_only_fields
class Meta:
model = Property
fields = '__all__'
read_only_fields = ['id', 'created_at', 'updated_at']
# Method 3: extra_kwargs
class Meta:
model = Property
fields = '__all__'
extra_kwargs = {
'created_at': {'read_only': True},
}Read-only fields are included in output (serialization) but ignored on input (deserialization).
21. How do you make a field write-only in a serializer?
Write-only fields are accepted on input but never included in output. Perfect for passwords.
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, min_length=8)
password_confirm = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['username', 'email', 'password', 'password_confirm']
def validate(self, data):
if data['password'] != data['password_confirm']:
raise serializers.ValidationError("Passwords don't match")
return data
def create(self, validated_data):
validated_data.pop('password_confirm')
user = User.objects.create_user(**validated_data)
return user22. What is the `source` argument in a serializer field?
source maps a serializer field name to a different model attribute or related field.
class PropertySerializer(serializers.ModelSerializer):
# Rename field in API output
property_title = serializers.CharField(source='title')
# Access related model attribute
owner_name = serializers.CharField(source='owner.get_full_name')
# Access nested relationship
city_name = serializers.CharField(source='location.city.name')
class Meta:
model = Property
fields = ['property_title', 'owner_name', 'city_name']23. How do you validate a single field using `validate_<field>` method?
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = ['title', 'price', 'zip_code']
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError("Price must be positive.")
if value > 100_000_000:
raise serializers.ValidationError("Price seems unrealistic.")
return value
def validate_zip_code(self, value):
if not value.isdigit() or len(value) != 5:
raise serializers.ValidationError("German ZIP must be 5 digits.")
return value24. How do you perform object-level validation in a serializer?
Use the validate() method — it receives all fields together, letting you compare them.
class BookingSerializer(serializers.ModelSerializer):
class Meta:
model = Booking
fields = ['property', 'check_in', 'check_out', 'guests']
def validate(self, data):
if data['check_in'] >= data['check_out']:
raise serializers.ValidationError(
"Check-out must be after check-in."
)
if data['guests'] > data['property'].max_guests:
raise serializers.ValidationError(
f"This property allows max {data['property'].max_guests} guests."
)
return data25. What is the difference between `create()` and `update()` in a serializer?
create() is called when you call serializer.save() on a new instance (no instance passed). update() is called when you pass an existing instance.
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = ['title', 'price', 'owner']
def create(self, validated_data):
# Called by: serializer.save()
# Can inject extra data here
return Property.objects.create(**validated_data)
def update(self, instance, validated_data):
# Called by: serializer.save() when instance is passed
instance.title = validated_data.get('title', instance.title)
instance.price = validated_data.get('price', instance.price)
instance.save()
return instance# In the view:
# CREATE
serializer = PropertySerializer(data=request.data)
serializer.save() # calls create()
# UPDATE
serializer = PropertySerializer(property, data=request.data, partial=True)
serializer.save() # calls update()26. How do you handle nested serializers?
class OwnerSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email']
class PropertySerializer(serializers.ModelSerializer):
owner = OwnerSerializer(read_only=True) # Nested output
owner_id = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(),
source='owner',
write_only=True
)
class Meta:
model = Property
fields = ['id', 'title', 'owner', 'owner_id']This gives: read as nested object, write with just an ID — the most practical pattern.
27. What is `many=True` used for in serializers?
It tells the serializer to handle a list of objects instead of a single one. Internally creates a ListSerializer.
# Serializing multiple objects
properties = Property.objects.all()
serializer = PropertySerializer(properties, many=True)
# serializer.data → [{"id": 1, ...}, {"id": 2, ...}]
# In a view
class PropertyListView(APIView):
def get(self, request):
properties = Property.objects.filter(is_active=True)
serializer = PropertySerializer(properties, many=True)
return Response(serializer.data)28. How do you serialize a queryset vs a single object?
# Single object — no many=True
property = Property.objects.get(pk=1)
serializer = PropertySerializer(property)
# data: {"id": 1, "title": "..."}
# Queryset — many=True required
properties = Property.objects.all()
serializer = PropertySerializer(properties, many=True)
# data: [{"id": 1, ...}, {"id": 2, ...}]29. What does `validated_data` contain and when is it available?
validated_data is the cleaned, Python-native data after is_valid() runs. It's only available after validation passes.
class PropertyView(APIView):
def post(self, request):
serializer = PropertySerializer(data=request.data)
# Before is_valid() — validated_data doesn't exist yet
if serializer.is_valid():
# Now validated_data is available
print(serializer.validated_data)
# {'title': 'Modern Flat', 'price': Decimal('250000.00')}
serializer.save()
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)30. How do you add custom fields not in the model to a serializer?
class PropertySerializer(serializers.ModelSerializer):
# Computed field
price_per_sqm = serializers.SerializerMethodField()
# Field not on model but in validated_data for write
promo_code = serializers.CharField(write_only=True, required=False)
class Meta:
model = Property
fields = ['id', 'title', 'price', 'area_sqm', 'price_per_sqm', 'promo_code']
def get_price_per_sqm(self, obj):
if obj.area_sqm:
return round(obj.price / obj.area_sqm, 2)
return None
def create(self, validated_data):
promo_code = validated_data.pop('promo_code', None)
instance = super().create(validated_data)
if promo_code:
apply_discount(instance, promo_code)
return instance31. What is `SerializerMethodField` and when do you use it?
SerializerMethodField lets you add a read-only computed field backed by a method. Perfect for any value that's derived rather than stored.
class PropertySerializer(serializers.ModelSerializer):
is_premium = serializers.SerializerMethodField()
full_address = serializers.SerializerMethodField()
days_listed = serializers.SerializerMethodField()
class Meta:
model = Property
fields = ['id', 'title', 'price', 'is_premium', 'full_address', 'days_listed']
def get_is_premium(self, obj):
return obj.price > 500_000
def get_full_address(self, obj):
return f"{obj.street}, {obj.zip_code} {obj.city}"
def get_days_listed(self, obj):
from django.utils import timezone
return (timezone.now() - obj.created_at).days32. How do you control which fields are included using `fields` in Meta?
# Explicit list — most recommended
class PropertyListSerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = ['id', 'title', 'price', 'city'] # Only these 4
# All fields — use carefully
class PropertyAdminSerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = '__all__' # Every model fieldExplicit field lists are safer — they don't accidentally expose new model fields when you add them.
33. How do you exclude specific fields using `exclude` in Meta?
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property
exclude = ['internal_notes', 'admin_score', 'raw_data']
# Everything except these fieldsYou can't use both fields and exclude together.
34. What is `extra_kwargs` in `ModelSerializer.Meta`?
extra_kwargs lets you add or override field options without redefining the field.
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = ['id', 'title', 'price', 'slug', 'owner', 'created_at']
extra_kwargs = {
'slug': {'read_only': True},
'created_at': {'read_only': True},
'price': {
'min_value': 0,
'max_value': 100_000_000,
'error_messages': {'min_value': 'Price cannot be negative.'}
},
'owner': {'write_only': True},
}35. How do you handle file uploads in a serializer?
class PropertyImageSerializer(serializers.ModelSerializer):
image = serializers.ImageField()
class Meta:
model = PropertyImage
fields = ['id', 'property', 'image', 'caption']
def validate_image(self, value):
max_size = 5 * 1024 * 1024 # 5MB
if value.size > max_size:
raise serializers.ValidationError("Image must be under 5MB.")
return value# View must use MultiPartParser
class PropertyImageUploadView(APIView):
parser_classes = [MultiPartParser, FormParser]
def post(self, request):
serializer = PropertyImageSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)36. What is a `HyperlinkedModelSerializer`?
Instead of primary keys for relationships, it uses URLs. Better for REST purists; makes APIs self-documenting.
class PropertySerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Property
fields = ['url', 'id', 'title', 'owner']
# owner → "http://api.example.com/users/5/" instead of 5In practice, most teams use ModelSerializer with PKs — simpler and more predictable.
37. How does DRF handle `UniqueValidator` and `UniqueTogetherValidator`?
DRF adds these automatically from your model's unique and unique_together constraints.
# Automatic from model
class Property(models.Model):
slug = models.SlugField(unique=True)
class Meta:
unique_together = [['owner', 'title']]
# DRF auto-adds validators — or manually:
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = ['slug', 'owner', 'title']
validators = [
UniqueTogetherValidator(
queryset=Property.objects.all(),
fields=['owner', 'title'],
message="You already have a property with this title."
)
]38. What is `to_representation()` and when would you override it?
to_representation() controls the final output format. Override it to transform data after normal serialization.
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = ['id', 'title', 'price', 'currency']
def to_representation(self, instance):
data = super().to_representation(instance)
# Format price with currency
data['price'] = f"{data['currency']} {data['price']:,.2f}"
data.pop('currency') # Remove raw currency field
# Add computed field not worth a SerializerMethodField
data['url'] = f"/properties/{instance.pk}/"
return data39. What is `to_internal_value()` and when would you override it?
to_internal_value() handles input transformation before validation. Override to normalize or transform incoming data.
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = ['title', 'price']
def to_internal_value(self, data):
# Normalize: convert German number format "250.000,99" → "250000.99"
if 'price' in data and isinstance(data['price'], str):
data = data.copy()
data['price'] = data['price'].replace('.', '').replace(',', '.')
return super().to_internal_value(data)40. How do you pass extra context to a serializer?
Use the context parameter. Common uses: passing request, user, or view-specific data.
# In the view
serializer = PropertySerializer(
property,
context={'request': request, 'show_private': request.user.is_staff}
)
# In the serializer
class PropertySerializer(serializers.ModelSerializer):
private_notes = serializers.SerializerMethodField()
def get_private_notes(self, obj):
if self.context.get('show_private'):
return obj.private_notes
return None41. How do you use `partial=True` in a serializer for PATCH requests?
partial=True makes all fields optional, so PATCH can update just one field without sending everything.
class PropertyView(APIView):
def patch(self, request, pk):
property = get_object_or_404(Property, pk=pk)
# partial=True → only validate/update provided fields
serializer = PropertySerializer(
property,
data=request.data,
partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=400)PUT without partial=True requires all fields. PATCH with partial=True allows any subset.
42. What is the difference between `is_valid()` with `raise_exception=True` vs without?
# Without raise_exception — manual handling
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors, status=400)
# With raise_exception=True — auto 400 on invalid
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
# If invalid → DRF raises ValidationError → returns 400 automaticallyraise_exception=True is cleaner for most cases.
43. How do you handle forward relationships (FK, M2M) in serializers?
class PropertySerializer(serializers.ModelSerializer):
# By default: FK → returns PK integer
owner = serializers.PrimaryKeyRelatedField(queryset=User.objects.all())
# As nested object (read)
owner_detail = OwnerSerializer(source='owner', read_only=True)
# As string
owner_name = serializers.StringRelatedField(source='owner')
# M2M as list of PKs
amenities = serializers.PrimaryKeyRelatedField(
many=True, queryset=Amenity.objects.all()
)
class Meta:
model = Property
fields = ['id', 'title', 'owner', 'owner_detail', 'owner_name', 'amenities']44. How do you serialize a `GenericRelatedField`?
When you have a GenericForeignKey and need to serialize it as different types:
class GenericObjectRelatedField(serializers.RelatedField):
def to_representation(self, value):
if isinstance(value, Property):
return PropertySerializer(value).data
if isinstance(value, User):
return UserSerializer(value).data
raise Exception(f'Unexpected type: {type(value)}')
class ActivitySerializer(serializers.ModelSerializer):
target = GenericObjectRelatedField(read_only=True)
class Meta:
model = Activity
fields = ['id', 'verb', 'target', 'created_at']45. What are `ListSerializer` and `BaseSerializer` used for?
ListSerializer is what gets created when you use many=True. Override it for bulk create/update logic.
class PropertyListSerializer(serializers.ListSerializer):
def create(self, validated_data):
properties = [Property(**item) for item in validated_data]
return Property.objects.bulk_create(properties)
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = ['title', 'price']
list_serializer_class = PropertyListSerializer
# Usage: bulk create
serializer = PropertySerializer(data=request.data, many=True)
if serializer.is_valid():
serializer.save() # Calls bulk_create → single DB queryBaseSerializer is the base class when you need full control over both in and out, with no model.
🔭 Views & ViewSets
46. What is a `ViewSet` and how does it differ from `APIView`?
APIView maps HTTP methods to functions (get, post, etc.). ViewSet maps actions (list, create, retrieve, update, destroy) and connects to routers automatically.
from rest_framework import viewsets
class PropertyViewSet(viewsets.ViewSet):
def list(self, request): # GET /properties/
...
def create(self, request): # POST /properties/
...
def retrieve(self, request, pk): # GET /properties/{pk}/
...
def update(self, request, pk): # PUT /properties/{pk}/
...
def destroy(self, request, pk): # DELETE /properties/{pk}/
...47. What is a `ModelViewSet` and what actions does it provide by default?
ModelViewSet gives you all 5 CRUD actions with minimal code:
class PropertyViewSet(viewsets.ModelViewSet):
queryset = Property.objects.all()
serializer_class = PropertySerializer
permission_classes = [IsAuthenticated]
# This single class handles:
# GET /properties/ → list()
# POST /properties/ → create()
# GET /properties/{pk}/ → retrieve()
# PUT /properties/{pk}/ → update()
# PATCH /properties/{pk}/ → partial_update()
# DELETE /properties/{pk}/ → destroy()48. What are the individual mixins used for?
Mixins let you compose only the actions you need:
from rest_framework import mixins, generics
# Read-only list + detail (no create/update/delete)
class PropertyReadOnlyViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet
):
queryset = Property.objects.all()
serializer_class = PropertySerializer
# Create + List only (no update/delete)
class CommentViewSet(
mixins.CreateModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet
):
queryset = Comment.objects.all()
serializer_class = CommentSerializer49. What is `GenericAPIView` and what does it provide?
GenericAPIView extends APIView with queryset, serializer, pagination, filtering, and lookup wiring. All generic views and mixins are built on it.
from rest_framework.generics import GenericAPIView
class PropertyView(GenericAPIView):
queryset = Property.objects.all()
serializer_class = PropertySerializer
def get(self, request):
qs = self.get_queryset() # filtered queryset
serializer = self.get_serializer(qs, many=True)
return Response(serializer.data)50. What is `get_queryset()` and why override it vs defining `queryset` directly?
queryset is static — evaluated once at class definition. get_queryset() is dynamic — called per request.
class PropertyViewSet(viewsets.ModelViewSet):
serializer_class = PropertySerializer
def get_queryset(self):
# Filter by current user → needs request context
return Property.objects.filter(
owner=self.request.user,
is_active=True
).select_related('owner', 'location')
# Cannot do this with a class-level queryset attributeAlways override get_queryset() when you need self.request or dynamic filtering.
51. What is `get_serializer_class()` and when is it useful?
Returns different serializers based on the action, user role, or request context.
class PropertyViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.action == 'list':
return PropertyListSerializer # Lightweight for lists
if self.action == 'create':
return PropertyCreateSerializer # Extra validation for creation
if self.request.user.is_staff:
return PropertyAdminSerializer # More fields for admins
return PropertyDetailSerializer # Default52. How do you restrict a ViewSet to only certain actions?
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
# Only list and retrieve — no create/update/delete
router.register('properties', PropertyViewSet, basename='property')
# In the ViewSet, use http_method_names or mixins:
class PropertyViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet
):
queryset = Property.objects.all()
serializer_class = PropertySerializer53. What is `get_object()` and how does it work?
get_object() fetches a single model instance using lookup_field (default: pk), applies object-level permissions, and raises 404 if not found.
class PropertyDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Property.objects.all()
serializer_class = PropertySerializer
def get(self, request, pk):
# get_object() handles: lookup by pk, 404, permission check
property = self.get_object()
serializer = self.get_serializer(property)
return Response(serializer.data)54. How do you override `perform_create()` in a view?
perform_create() is the hook for injecting extra data at save time without changing the serializer.
class PropertyViewSet(viewsets.ModelViewSet):
serializer_class = PropertySerializer
def perform_create(self, serializer):
# Inject owner from the authenticated user
serializer.save(
owner=self.request.user,
created_ip=self.request.META.get('REMOTE_ADDR')
)55. How do you override `perform_update()` and `perform_destroy()`?
class PropertyViewSet(viewsets.ModelViewSet):
def perform_update(self, serializer):
serializer.save(last_modified_by=self.request.user)
def perform_destroy(self, instance):
# Soft delete instead of hard delete
instance.is_deleted = True
instance.deleted_by = self.request.user
instance.save()
# Don't call instance.delete()56. What is `ReadOnlyModelViewSet`?
A ModelViewSet with only list and retrieve — read-only API endpoints.
class CityViewSet(viewsets.ReadOnlyModelViewSet):
queryset = City.objects.all()
serializer_class = CitySerializer
permission_classes = [AllowAny]
# Gives:
# GET /cities/ → list
# GET /cities/{pk}/ → retrieve
# No POST, PUT, PATCH, DELETE57. How do you add custom actions to a ViewSet using `@action`?
from rest_framework.decorators import action
class PropertyViewSet(viewsets.ModelViewSet):
queryset = Property.objects.all()
serializer_class = PropertySerializer
@action(detail=True, methods=['post'], url_path='publish')
def publish(self, request, pk=None):
property = self.get_object()
property.is_published = True
property.save()
return Response({'status': 'published'})
@action(detail=False, methods=['get'], url_path='featured')
def featured(self, request):
featured = Property.objects.filter(is_featured=True)
serializer = self.get_serializer(featured, many=True)
return Response(serializer.data)
# URLs generated:
# POST /properties/{pk}/publish/
# GET /properties/featured/58. What is the `detail` parameter in `@action`?
- detail=True → operates on a single object, URL includes {pk}: /properties/{pk}/publish/ - detail=False → operates on the collection: /properties/featured/
59. How do you return different serializers for list vs detail views?
class PropertyViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.action == 'list':
return PropertyListSerializer # Fewer fields, faster
return PropertyDetailSerializer # All fields# PropertyListSerializer — lean
class PropertyListSerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = ['id', 'title', 'price', 'city', 'thumbnail']
# PropertyDetailSerializer — full
class PropertyDetailSerializer(serializers.ModelSerializer):
owner = OwnerSerializer(read_only=True)
images = PropertyImageSerializer(many=True, read_only=True)
class Meta:
model = Property
fields = '__all__'60. How do you return a custom response from a ViewSet action?
@action(detail=True, methods=['get'])
def price_history(self, request, pk=None):
property = self.get_object()
history = PriceHistory.objects.filter(property=property).order_by('-date')
data = {
'property_id': pk,
'current_price': property.price,
'history': [
{'date': h.date, 'price': str(h.price)} for h in history
]
}
return Response(data)🔐 Authentication & Permissions
71. What authentication classes does DRF provide out of the box?
| Class | Mechanism | Use Case | |-------|-----------|----------| | SessionAuthentication | Django session cookie | Browser-based clients | | TokenAuthentication | Authorization: Token header | Mobile/SPA clients | | BasicAuthentication | Base64 username:password | Testing only | | RemoteUserAuthentication | Server-provided REMOTE_USER | SSO/enterprise |
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
]
}72. How does `SessionAuthentication` work in DRF?
It uses Django's built-in session framework. After login, Django creates a session cookie. DRF reads that cookie to identify the user.
Requires CSRF enforcement — DRF adds CSRF checks automatically for SessionAuthentication.
# Login
from django.contrib.auth import login
class LoginView(APIView):
authentication_classes = []
permission_classes = [AllowAny]
def post(self, request):
user = authenticate(
username=request.data['username'],
password=request.data['password']
)
if user:
login(request, user) # Creates session
return Response({'status': 'logged in'})
return Response({'error': 'Invalid credentials'}, status=401)73. How does `TokenAuthentication` work and how do you set it up?
# 1. Add to INSTALLED_APPS
INSTALLED_APPS = [..., 'rest_framework.authtoken']
# 2. Run migration
# python manage.py migrate
# 3. Generate token on login
from rest_framework.authtoken.models import Token
class LoginView(APIView):
permission_classes = [AllowAny]
def post(self, request):
user = authenticate(**request.data)
if user:
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key})
return Response({'error': 'Invalid'}, status=401)
# 4. Client sends: Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4f76. What is the difference between authentication and permissions in DRF?
- Authentication = Who are you? — identifies the user from the request - Permissions = What are you allowed to do? — decides if the identified user can perform this action
class PropertyView(APIView):
authentication_classes = [TokenAuthentication] # WHO: identify via token
permission_classes = [IsAuthenticated] # WHAT: must be logged in
def get(self, request, pk):
# At this point:
# request.user is set (authentication passed)
# User is confirmed authenticated (permission passed)
property = get_object_or_404(Property, pk=pk)
return Response(PropertySerializer(property).data)80. How do you write a custom permission class?
from rest_framework.permissions import BasePermission
class IsPropertyOwner(BasePermission):
message = "You must be the owner of this property."
def has_permission(self, request, view):
# View-level: must be authenticated
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
# Object-level: must own the property
if request.method in ['GET', 'HEAD', 'OPTIONS']:
return True # Safe methods allowed
return obj.owner == request.user
class PropertyViewSet(viewsets.ModelViewSet):
permission_classes = [IsPropertyOwner]81. What is `has_permission()` vs `has_object_permission()`?
- has_permission() → checked on every request before the view runs - has_object_permission() → checked only when get_object() is called (detail views)
class IsOwnerOrReadOnly(BasePermission):
def has_permission(self, request, view):
# Everyone can read, authenticated users can write
if request.method in SAFE_METHODS:
return True
return request.user.is_authenticated
def has_object_permission(self, request, view, obj):
# Reading: anyone
if request.method in SAFE_METHODS:
return True
# Writing: only owner
return obj.owner == request.user⚡ Throttling, Filtering & Pagination
91. What is throttling in DRF and why is it important?
Throttling limits how many requests a client can make in a given time window. Without it, your API is vulnerable to abuse, scraping, and denial-of-service.
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day',
}
}93. How do you create a custom throttle class?
from rest_framework.throttling import UserRateThrottle
class AIAnalysisThrottle(UserRateThrottle):
scope = 'ai_analysis'
# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES': {
'ai_analysis': '10/day', # Only 10 AI calls per user per day
}
}
# View
class ImmoCheckView(APIView):
throttle_classes = [AIAnalysisThrottle]101. What is `PageNumberPagination`?
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
}
# Custom class for more control
class PropertyPagination(PageNumberPagination):
page_size = 20
page_size_query_param = 'page_size' # ?page_size=50
max_page_size = 100
page_query_param = 'page' # ?page=2
# Response:
# {
# "count": 150,
# "next": "http://api.example.com/properties/?page=3",
# "previous": "http://api.example.com/properties/?page=1",
# "results": [...]
# }102. What is `LimitOffsetPagination`?
class PropertyPagination(LimitOffsetPagination):
default_limit = 20
max_limit = 100
# Client uses: ?limit=20&offset=40
# Response:
# {
# "count": 150,
# "next": "...?limit=20&offset=60",
# "previous": "...?limit=20&offset=20",
# "results": [...]
# }🗺️ Routers & URLs
111. What is a DRF `Router` and what problem does it solve?
Routers auto-generate URL patterns for ViewSets. Without them, you'd manually write 6 URL patterns per ViewSet.
# Without router — tedious
urlpatterns = [
path('properties/', PropertyViewSet.as_view({'get': 'list', 'post': 'create'})),
path('properties/<pk>/', PropertyViewSet.as_view({
'get': 'retrieve', 'put': 'update',
'patch': 'partial_update', 'delete': 'destroy'
})),
]
# With router — clean
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('properties', PropertyViewSet)
router.register('users', UserViewSet)
urlpatterns = [path('api/', include(router.urls))]112. What is the difference between `DefaultRouter` and `SimpleRouter`?
DefaultRouter adds an API root endpoint (/api/) that lists all registered routes. SimpleRouter doesn't.
In production, SimpleRouter is preferable — you don't want to expose your route map.
🧪 Testing
121. What is `APITestCase` and how does it differ from Django's `TestCase`?
APITestCase provides APIClient — which understands DRF authentication and formats requests properly for JSON APIs.
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
class PropertyAPITest(APITestCase):
def setUp(self):
self.user = User.objects.create_user('testuser', password='pass123')
self.property = Property.objects.create(
title='Test Property',
price=250000,
owner=self.user
)
def test_list_properties(self):
self.client.force_authenticate(user=self.user)
response = self.client.get('/api/properties/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 1)
def test_unauthenticated_access(self):
response = self.client.get('/api/properties/')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)🔄 Advanced & Real-World
134. What is `exception_handler` setting and how does it work?
# settings.py
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'myapp.utils.custom_exception_handler'
}
# utils.py
from rest_framework.views import exception_handler
from rest_framework.response import Response
def custom_exception_handler(exc, context):
# Call DRF's default handler first
response = exception_handler(exc, context)
if response is not None:
# Wrap in consistent format
response.data = {
'success': False,
'error': {
'code': response.status_code,
'message': response.data,
}
}
return response150. How do you optimize DRF serializer performance for large datasets using `select_related` and `prefetch_related`?
The N+1 problem is the most common DRF performance killer.
# BAD — N+1 queries: 1 for properties + N for each owner
class PropertyViewSet(viewsets.ModelViewSet):
queryset = Property.objects.all()
serializer_class = PropertySerializer
# GOOD — 2 queries total
class PropertyViewSet(viewsets.ModelViewSet):
serializer_class = PropertySerializer
def get_queryset(self):
return Property.objects.select_related(
'owner', # FK — single JOIN
'location', # FK
).prefetch_related(
'images', # Reverse FK — separate optimized query
'amenities', # M2M
).filter(is_active=True)# For SerializerMethodField that hits the DB — cache results
class PropertySerializer(serializers.ModelSerializer):
similar_count = serializers.SerializerMethodField()
def get_similar_count(self, obj):
# annotate this in the queryset instead:
return getattr(obj, 'similar_count', 0)
# In the viewset:
def get_queryset(self):
return Property.objects.annotate(
similar_count=Count('similar_properties')
)61. How do you get the current user inside a serializer?
Access it via self.context['request'].user:
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = ['id', 'title', 'price', 'is_owner']
is_owner = serializers.SerializerMethodField()
def get_is_owner(self, obj):
request = self.context.get('request')
if request and request.user.is_authenticated:
return obj.owner == request.user
return FalseAlways pass context={'request': request} when instantiating the serializer in your view — or use get_serializer() which does it automatically.
62. How do you handle writable nested serializers?
By default, nested serializers are read-only. To make them writable, override create() and update():
class AddressSerializer(serializers.ModelSerializer):
class Meta:
model = Address
fields = ['street', 'city', 'zip_code', 'country']
class PropertySerializer(serializers.ModelSerializer):
address = AddressSerializer()
class Meta:
model = Property
fields = ['id', 'title', 'price', 'address']
def create(self, validated_data):
address_data = validated_data.pop('address')
address = Address.objects.create(**address_data)
property = Property.objects.create(address=address, **validated_data)
return property
def update(self, instance, validated_data):
address_data = validated_data.pop('address', None)
if address_data:
for attr, value in address_data.items():
setattr(instance.address, attr, value)
instance.address.save()
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance63. What is the difference between `PUT` and `PATCH` in DRF views?
- PUT = full update — all required fields must be sent, missing fields get reset to default - PATCH = partial update — only send the fields you want to change
class PropertyView(APIView):
def put(self, request, pk):
property = get_object_or_404(Property, pk=pk)
# partial=False (default) — requires ALL fields
serializer = PropertySerializer(property, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=400)
def patch(self, request, pk):
property = get_object_or_404(Property, pk=pk)
# partial=True — only validate/update provided fields
serializer = PropertySerializer(property, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=400)In ModelViewSet, update() handles PUT and partial_update() handles PATCH automatically.
64. What is `get_serializer_context()` and when do you override it?
It builds the context dict passed to every serializer. Override to inject extra data:
class PropertyViewSet(viewsets.ModelViewSet):
def get_serializer_context(self):
context = super().get_serializer_context()
# Add extra context for the serializer to use
context['show_private_fields'] = self.request.user.is_staff
context['currency'] = self.request.query_params.get('currency', 'EUR')
return contextThe base method already adds request, format, and view — you're extending, not replacing.
65. How do you implement dynamic fields in a serializer (return different fields per request)?
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
fields = kwargs.pop('fields', None)
super().__init__(*args, **kwargs)
if fields is not None:
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
class PropertySerializer(DynamicFieldsModelSerializer):
class Meta:
model = Property
fields = ['id', 'title', 'price', 'owner', 'address', 'images']
# Usage: only return id + title + price
serializer = PropertySerializer(property, fields=['id', 'title', 'price'])Or via query param in the view:
def get_serializer(self, *args, **kwargs):
fields = self.request.query_params.get('fields')
if fields:
kwargs['fields'] = fields.split(',')
return super().get_serializer(*args, **kwargs)
# GET /properties/?fields=id,title,price66. What is `depth` in `ModelSerializer.Meta` and when should you avoid it?
depth auto-expands related objects to N levels deep:
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = '__all__'
depth = 1 # Expand FK relationships one level
# depth=1: owner becomes {"id": 1, "username": "alok", ...} instead of just 1
# depth=2: owner.profile also expandedAvoid it because: - No control over which fields of related objects are exposed - Can expose sensitive data accidentally - Can trigger N+1 queries - Read-only (can't write nested data)
Use explicit nested serializers instead — more control, safer.
67. How do you filter a queryset based on URL kwargs?
# URL: /api/owners/<owner_id>/properties/
class PropertyViewSet(viewsets.ModelViewSet):
serializer_class = PropertySerializer
def get_queryset(self):
owner_id = self.kwargs.get('owner_id')
if owner_id:
return Property.objects.filter(owner_id=owner_id)
return Property.objects.all()# urls.py
urlpatterns = [
path('owners/<int:owner_id>/properties/',
PropertyViewSet.as_view({'get': 'list'})),
]68. What is `action` attribute on the view and how do you use it?
self.action tells you which ViewSet action is currently running (list, create, retrieve, update, partial_update, destroy, or your custom action name):
class PropertyViewSet(viewsets.ModelViewSet):
def get_permissions(self):
# Different permissions for different actions
if self.action in ['list', 'retrieve']:
return [AllowAny()]
if self.action == 'destroy':
return [IsAdminUser()]
return [IsAuthenticated()]
def get_throttle_classes(self):
if self.action == 'create':
return [UserRateThrottle]
return []69. How do you add pagination to a specific ViewSet action?
class PropertyViewSet(viewsets.ModelViewSet):
@action(detail=False, methods=['get'])
def search(self, request):
queryset = Property.objects.filter(
title__icontains=request.query_params.get('q', '')
)
# Manually apply pagination
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)70. What does `finalize_response()` do in DRF?
It's called just before the response is returned. Useful for adding headers or post-processing:
class PropertyViewSet(viewsets.ModelViewSet):
def finalize_response(self, request, response, *args, **kwargs):
response = super().finalize_response(request, response, *args, **kwargs)
# Add custom header to every response from this viewset
response['X-App-Version'] = '2.1.0'
response['X-Response-Time'] = getattr(request, '_response_time', 'unknown')
return response74. What is `BasicAuthentication` and when should you avoid it?
BasicAuthentication sends base64-encoded username:password with every request in the Authorization header.
# Client sends: Authorization: Basic YWxvazpzZWNyZXQ=
# (base64 of "alok:secret")Avoid it because: - Credentials sent with EVERY request (not just login) - base64 is not encryption — trivially decodable - No token expiry mechanism - Only safe over HTTPS (still sends credentials constantly)
When it's OK: Internal tools, testing, machine-to-machine in controlled environments over HTTPS.
75. How do you implement JWT authentication in DRF?
pip install djangorestframework-simplejwt# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
}# urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('api/token/', TokenObtainPairView.as_view()), # POST: get tokens
path('api/token/refresh/', TokenRefreshView.as_view()), # POST: refresh
]# Get tokens
POST /api/token/ {"username": "alok", "password": "secret"}
# Response: {"access": "eyJ...", "refresh": "eyJ..."}
# Use access token
Authorization: Bearer eyJ...
# Refresh when expired
POST /api/token/refresh/ {"refresh": "eyJ..."}77. What is `IsAuthenticated` permission class?
Returns True only if request.user is authenticated (not anonymous):
from rest_framework.permissions import IsAuthenticated
class MyView(APIView):
permission_classes = [IsAuthenticated]
# 401 for unauthenticated, 403 if authenticated but no permission78. What is `IsAdminUser` permission class?
Returns True only if request.user.is_staff is True:
from rest_framework.permissions import IsAdminUser
class AdminOnlyView(APIView):
permission_classes = [IsAdminUser]
# Only Django staff users can access79. What is `IsAuthenticatedOrReadOnly` permission class?
Safe methods (GET, HEAD, OPTIONS) are allowed for anyone. Write methods require authentication:
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class PropertyViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticatedOrReadOnly]
# GET /properties/ → anyone
# POST /properties/ → must be authenticated82. How do you set permissions per-view vs globally?
# Global (settings.py) — applies to ALL views
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated']
}
# Per-view — overrides global
class PublicListingsView(generics.ListAPIView):
permission_classes = [AllowAny] # Public endpoint
class AdminView(generics.ListAPIView):
permission_classes = [IsAdminUser] # Admin only83. What is `AllowAny` and when would you use it?
AllowAny explicitly grants access to all users — authenticated or not. Use when you want to be explicit about a public endpoint, even though the global default requires auth:
class RegisterView(APIView):
permission_classes = [AllowAny] # Explicit — no auth needed to register
class HealthCheckView(APIView):
permission_classes = [AllowAny] # Public health check endpoint84. How do you combine multiple permissions (AND logic) in DRF?
Listing multiple permission classes applies AND logic — all must pass:
class PropertyView(APIView):
permission_classes = [IsAuthenticated, IsVerifiedUser, IsActiveSubscriber]
# User must be: authenticated AND verified AND subscribedFor OR logic, you need a custom class:
from rest_framework.permissions import BasePermission, IsAuthenticated, IsAdminUser
class IsAuthenticatedOrAdmin(BasePermission):
def has_permission(self, request, view):
return (IsAuthenticated().has_permission(request, view) or
IsAdminUser().has_permission(request, view))85. How do you implement row-level or object-level permissions?
Override has_object_permission(). It's only called for detail endpoints (retrieve, update, destroy):
class IsOwnerOrAdmin(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in ['GET', 'HEAD', 'OPTIONS']:
return True
return obj.owner == request.user or request.user.is_staff
class PropertyViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsOwnerOrAdmin]
# get_object() automatically calls has_object_permission()Important: has_object_permission is NOT called on list views — only when get_object() is called. Filter querysets manually for list views.
86. What is `request.user` and `request.auth` in DRF?
- request.user → The authenticated Django User instance (or AnonymousUser) - request.auth → The raw auth credential (Token instance, JWT payload, None)
class MyView(APIView):
def get(self, request):
print(request.user) # <User: alok>
print(request.auth) # Token instance (for TokenAuthentication)
# or JWT dict (for JWTAuthentication)
print(request.user.is_authenticated) # True/False87. How do you test authenticated endpoints in DRF tests?
from rest_framework.test import APITestCase
from rest_framework.authtoken.models import Token
class PropertyTests(APITestCase):
def setUp(self):
self.user = User.objects.create_user('alok', password='pass')
# Method 1: force_authenticate (most common)
def test_create_property(self):
self.client.force_authenticate(user=self.user)
response = self.client.post('/api/properties/', {'title': 'Test', 'price': 100000})
self.assertEqual(response.status_code, 201)
# Method 2: Token credentials
def test_with_token(self):
token = Token.objects.create(user=self.user)
self.client.credentials(HTTP_AUTHORIZATION=f'Token {token.key}')
response = self.client.get('/api/properties/')
self.assertEqual(response.status_code, 200)
# Method 3: JWT
def test_with_jwt(self):
from rest_framework_simplejwt.tokens import RefreshToken
refresh = RefreshToken.for_user(self.user)
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}')
response = self.client.get('/api/properties/')
self.assertEqual(response.status_code, 200)88. What is `DjangoModelPermissions`?
Maps standard Django model permissions (add_, change_, delete_, view_) to HTTP methods:
class PropertyViewSet(viewsets.ModelViewSet):
permission_classes = [DjangoModelPermissions]
# POST → requires app.add_property
# PUT → requires app.change_property
# DELETE → requires app.delete_property
# GET → requires app.view_propertyGreat for admin-style APIs where permissions are managed through Django's admin interface.
89. How do you throttle anonymous vs authenticated users differently?
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '20/day', # Anonymous: 20 requests per day
'user': '1000/day', # Authenticated: 1000 per day
}
}90. How does DRF handle CSRF protection with `SessionAuthentication`?
DRF enforces CSRF for SessionAuthentication only — not for Token/JWT auth. For AJAX requests from browsers using session auth:
# Django sets CSRF cookie automatically
# Include it in your AJAX headers:
# X-CSRFToken: <value from csrftoken cookie>
# In React/Vue with axios:
import Cookies from 'js-cookie'
axios.defaults.headers.common['X-CSRFToken'] = Cookies.get('csrftoken')For SPA + session auth, either set CSRF_COOKIE_HTTPONLY = False so JS can read the cookie, or use Token/JWT authentication instead.
92. What is `AnonRateThrottle` vs `UserRateThrottle`?
- AnonRateThrottle — identifies anonymous users by IP address, applies rate from 'anon' key - UserRateThrottle — identifies authenticated users by user ID, applies rate from 'user' key
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '5000/day',
}
}94. What is `ScopedRateThrottle`?
Lets you set different rates for different views using a throttle_scope attribute:
class LoginView(APIView):
throttle_scope = 'login' # Uses 'login' rate
class AIAnalysisView(APIView):
throttle_scope = 'ai' # Uses 'ai' rate
class StandardView(APIView):
throttle_scope = 'standard'REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': ['rest_framework.throttling.ScopedRateThrottle'],
'DEFAULT_THROTTLE_RATES': {
'login': '5/hour',
'ai': '10/day',
'standard': '1000/day',
}
}95. How do you set throttle rates globally vs per-view?
# Global (settings.py)
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': ['rest_framework.throttling.UserRateThrottle'],
'DEFAULT_THROTTLE_RATES': {'user': '1000/day'}
}
# Per-view override
class ExpensiveView(APIView):
throttle_classes = [UserRateThrottle]
throttle_scope = 'expensive' # if using ScopedRateThrottle
class NoThrottleView(APIView):
throttle_classes = [] # No throttling for this view96. What is `django-filter` and how does it integrate with DRF?
django-filter provides declarative filtering for querysets. DRF integrates it via DjangoFilterBackend:
pip install django-filter# settings.py
INSTALLED_APPS = [..., 'django_filters']
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}# filters.py
import django_filters
class PropertyFilter(django_filters.FilterSet):
min_price = django_filters.NumberFilter(field_name='price', lookup_expr='gte')
max_price = django_filters.NumberFilter(field_name='price', lookup_expr='lte')
city = django_filters.CharFilter(field_name='location__city', lookup_expr='iexact')
year_built = django_filters.RangeFilter()
class Meta:
model = Property
fields = ['property_type', 'is_available', 'min_price', 'max_price']
class PropertyViewSet(viewsets.ModelViewSet):
filterset_class = PropertyFilter
# GET /properties/?min_price=200000&max_price=500000&city=Munich97. How do you use `filterset_fields` in a view?
Quick way to add exact-match filtering without writing a FilterSet:
class PropertyViewSet(viewsets.ModelViewSet):
filterset_fields = ['city', 'property_type', 'is_available', 'owner']
# GET /properties/?city=Munich&property_type=apartment&is_available=trueFor more complex lookups (ranges, contains), use a full FilterSet class.
98. What is `SearchFilter` and how do you configure it?
SearchFilter adds ?search=term functionality across multiple fields:
from rest_framework.filters import SearchFilter
class PropertyViewSet(viewsets.ModelViewSet):
filter_backends = [SearchFilter]
search_fields = [
'title', # Exact contains match
'^city', # Starts-with match
'=zip_code', # Exact match
'@description', # Full-text search (PostgreSQL only)
'owner__username', # Related field
]
# GET /properties/?search=munich apartment
# Searches title, city, zip_code, description, owner.username99. What is `OrderingFilter` and how do you use it?
Lets clients sort results via ?ordering=field:
from rest_framework.filters import OrderingFilter
class PropertyViewSet(viewsets.ModelViewSet):
filter_backends = [OrderingFilter]
ordering_fields = ['price', 'created_at', 'title', 'area_sqm']
ordering = ['-created_at'] # Default ordering
# GET /properties/?ordering=price → cheapest first
# GET /properties/?ordering=-price → most expensive first
# GET /properties/?ordering=city,price → by city, then price100. How do you write a custom `FilterBackend`?
from rest_framework.filters import BaseFilterBackend
class IsOwnerFilterBackend(BaseFilterBackend):
"""Only return objects owned by the current user."""
def filter_queryset(self, request, queryset, view):
return queryset.filter(owner=request.user)
class ActiveOnlyFilterBackend(BaseFilterBackend):
"""Only return active objects unless admin."""
def filter_queryset(self, request, queryset, view):
if not request.user.is_staff:
return queryset.filter(is_active=True)
return queryset
class PropertyViewSet(viewsets.ModelViewSet):
filter_backends = [IsOwnerFilterBackend, DjangoFilterBackend, OrderingFilter]103. What is `CursorPagination` and when is it preferred?
Cursor pagination uses an opaque cursor (encoded position) instead of page numbers. Ideal for real-time feeds where data changes frequently:
from rest_framework.pagination import CursorPagination
class PropertyFeedPagination(CursorPagination):
page_size = 20
cursor_query_param = 'cursor'
ordering = '-created_at' # Must be stable, unique ordering
class PropertyFeedViewSet(viewsets.ReadOnlyModelViewSet):
pagination_class = PropertyFeedPagination
# Response: {"next": "?cursor=cD0yMDIz...", "previous": null, "results": [...]}Use CursorPagination when: - Feed updates frequently (new items inserted between pages) - PageNumber pagination would skip/repeat items as data changes - You don't need "jump to page N" functionality
104. How do you set a custom page size per request?
class FlexiblePagination(PageNumberPagination):
page_size = 20
page_size_query_param = 'page_size' # Enable client to set size
max_page_size = 100 # Cap at 100
# Client: GET /properties/?page=2&page_size=50105. How do you override `get_paginated_response()` to customize output?
class CustomPagination(PageNumberPagination):
page_size = 20
def get_paginated_response(self, data):
return Response({
'meta': {
'total_count': self.page.paginator.count,
'total_pages': self.page.paginator.num_pages,
'current_page': self.page.number,
'page_size': self.page_size,
'has_next': self.page.has_next(),
'has_previous': self.page.has_previous(),
},
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link(),
},
'results': data
})106. How do you add pagination to a non-ModelViewSet view?
class PropertySearchView(APIView):
pagination_class = PageNumberPagination
def get(self, request):
queryset = Property.objects.filter(
title__icontains=request.query_params.get('q', '')
)
paginator = self.pagination_class()
paginator.page_size = 20
page = paginator.paginate_queryset(queryset, request)
serializer = PropertySerializer(page, many=True)
return paginator.get_paginated_response(serializer.data)107. What is the `filter_queryset()` method in `GenericAPIView`?
It applies all configured filter backends to the queryset. Called automatically in list() but must be called manually in custom actions:
class PropertyViewSet(viewsets.ModelViewSet):
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
@action(detail=False, methods=['get'])
def export(self, request):
# Must manually call filter_queryset to apply filters
queryset = self.filter_queryset(self.get_queryset())
# Now export the filtered data
return generate_csv_response(queryset)108. How do you implement search across related fields?
class PropertyViewSet(viewsets.ModelViewSet):
filter_backends = [SearchFilter]
search_fields = [
'title',
'description',
'location__city', # Related model field
'location__district',
'owner__username', # Related model field
'owner__company_name',
]For complex search (full-text, fuzzy), use PostgreSQL's search:
from django.contrib.postgres.search import SearchVector, SearchQuery
class PropertyViewSet(viewsets.ModelViewSet):
def get_queryset(self):
qs = Property.objects.all()
q = self.request.query_params.get('q')
if q:
qs = qs.annotate(
search=SearchVector('title', 'description', 'location__city')
).filter(search=SearchQuery(q))
return qs109. How do you combine multiple filter backends on the same view?
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend
class PropertyViewSet(viewsets.ModelViewSet):
filter_backends = [
DjangoFilterBackend, # ?city=Munich&property_type=apartment
SearchFilter, # ?search=modern kitchen
OrderingFilter, # ?ordering=-price
]
filterset_fields = ['city', 'property_type', 'is_available']
search_fields = ['title', 'description']
ordering_fields = ['price', 'area_sqm', 'created_at']
ordering = ['-created_at']
# All work together:
# GET /properties/?city=Munich&search=balcony&ordering=-price&page=2110. What are `lookup_field` and `lookup_url_kwarg`?
- lookup_field — model field used to look up a single object (default: pk) - lookup_url_kwarg — URL parameter name (default: same as lookup_field)
# Use slug instead of pk for URLs
class PropertyViewSet(viewsets.ModelViewSet):
lookup_field = 'slug' # Looks up by Property.slug
lookup_url_kwarg = 'slug' # URL: /properties/<slug>/
# urls.py with router — generates: /properties/<slug>/
router.register('properties', PropertyViewSet)
# Custom URL kwarg name different from field name
class PropertyViewSet(viewsets.ModelViewSet):
lookup_field = 'slug'
lookup_url_kwarg = 'property_slug' # URL: /properties/<property_slug>/113. How do you name router-generated URLs for use in `reverse()`?
router = DefaultRouter()
router.register('properties', PropertyViewSet, basename='property')
# DRF generates these URL names:
# property-list → /properties/
# property-detail → /properties/{pk}/
# property-{action} → /properties/{pk}/custom-action/
# Using reverse:
from rest_framework.reverse import reverse
url = reverse('property-list', request=request)
url = reverse('property-detail', args=[property.pk], request=request)114. When would you NOT use a router and write URLs manually?
- Non-standard URL patterns (nested resources without a nested router) - Function-based views with @api_view - When you need full control over URL names - Complex permission-based URL structures
# Manual — full control
urlpatterns = [
path('my-properties/', PropertyListView.as_view(), name='my-property-list'),
path('properties/<int:pk>/publish/', PropertyPublishView.as_view()),
path('search/', PropertySearchView.as_view(), name='property-search'),
]115. How do custom `@action` decorators affect router-generated URLs?
class PropertyViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'], url_path='publish', url_name='publish')
def publish(self, request, pk=None): ...
@action(detail=False, methods=['get'], url_path='featured', url_name='featured')
def featured(self, request): ...
# Generated URLs:
# POST /properties/{pk}/publish/ → name: property-publish
# GET /properties/featured/ → name: property-featured
# reverse:
reverse('property-publish', args=[pk])
reverse('property-featured')116. How do you version your API using URL-based versioning?
# settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2'],
'VERSION_PARAM': 'version',
}
# urls.py
urlpatterns = [
path('api/<str:version>/', include('api.urls')),
]
# In view — access version
class PropertyViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.request.version == 'v2':
return PropertySerializerV2
return PropertySerializer117. How do you use `APIClient` to make requests in tests?
from rest_framework.test import APIClient
client = APIClient()
# GET
response = client.get('/api/properties/')
# POST with JSON
response = client.post('/api/properties/', {'title': 'Test', 'price': 200000}, format='json')
# POST with multipart (file upload)
with open('image.jpg', 'rb') as f:
response = client.post('/api/upload/', {'image': f}, format='multipart')
# With auth
client.force_authenticate(user=user)
# or
client.credentials(HTTP_AUTHORIZATION='Bearer <token>')118. How do you authenticate a user in a DRF test?
Three ways — use force_authenticate for most tests:
class PropertyTests(APITestCase):
def setUp(self):
self.user = User.objects.create_user('alok', password='pass123')
def test_with_force_authenticate(self):
self.client.force_authenticate(user=self.user)
# Now all requests act as self.user
def test_with_token(self):
token = Token.objects.create(user=self.user)
self.client.credentials(HTTP_AUTHORIZATION=f'Token {token.key}')
def test_unauthenticated(self):
self.client.force_authenticate(user=None) # Reset to anon119. How do you test file uploads in DRF?
from django.test import override_settings
from django.core.files.uploadedfile import SimpleUploadedFile
class UploadTests(APITestCase):
@override_settings(MEDIA_ROOT='/tmp/test-media/')
def test_upload_image(self):
self.client.force_authenticate(user=self.user)
image = SimpleUploadedFile(
'property.jpg',
b'fake-image-content',
content_type='image/jpeg'
)
response = self.client.post(
'/api/images/',
{'property': 1, 'image': image},
format='multipart'
)
self.assertEqual(response.status_code, 201)120. How do you assert specific response status codes and data?
from rest_framework import status
class PropertyTests(APITestCase):
def test_create_property(self):
self.client.force_authenticate(user=self.user)
data = {'title': 'Luxury Flat', 'price': 450000, 'city': 'Munich'}
response = self.client.post('/api/properties/', data, format='json')
# Status code
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# Response data
self.assertEqual(response.data['title'], 'Luxury Flat')
self.assertEqual(response.data['price'], '450000.00')
self.assertIn('id', response.data)
# Database
self.assertEqual(Property.objects.count(), 1)
self.assertEqual(Property.objects.first().owner, self.user)122. How do you test custom permissions?
class IsOwnerPermissionTest(APITestCase):
def setUp(self):
self.owner = User.objects.create_user('owner', password='pass')
self.other = User.objects.create_user('other', password='pass')
self.property = Property.objects.create(
title='My Property', owner=self.owner
)
def test_owner_can_update(self):
self.client.force_authenticate(user=self.owner)
response = self.client.patch(
f'/api/properties/{self.property.pk}/',
{'title': 'Updated Title'}
)
self.assertEqual(response.status_code, 200)
def test_non_owner_cannot_update(self):
self.client.force_authenticate(user=self.other)
response = self.client.patch(
f'/api/properties/{self.property.pk}/',
{'title': 'Hacked Title'}
)
self.assertEqual(response.status_code, 403)123. How do you test throttling behavior?
from unittest.mock import patch
from django.core.cache import cache
class ThrottleTests(APITestCase):
def setUp(self):
cache.clear() # Clear throttle cache before each test
@override_settings(
REST_FRAMEWORK={
'DEFAULT_THROTTLE_CLASSES': ['rest_framework.throttling.AnonRateThrottle'],
'DEFAULT_THROTTLE_RATES': {'anon': '3/minute'}
}
)
def test_anon_throttle(self):
for i in range(3):
response = self.client.get('/api/properties/')
self.assertEqual(response.status_code, 200)
# 4th request should be throttled
response = self.client.get('/api/properties/')
self.assertEqual(response.status_code, 429) # Too Many Requests
self.assertIn('Retry-After', response.headers)124. How do you use `force_authenticate()` vs `credentials()`?
# force_authenticate — bypasses authentication completely, sets user directly
# Best for: most unit tests (fast, no token setup needed)
self.client.force_authenticate(user=self.user)
# credentials — sends actual auth headers, goes through auth classes
# Best for: testing authentication itself
token = Token.objects.create(user=self.user)
self.client.credentials(HTTP_AUTHORIZATION=f'Token {token.key}')
# Reset credentials
self.client.credentials() # Clear all credentials
self.client.force_authenticate(user=None) # Force anonymous125. How do you mock external API calls in DRF tests?
from unittest.mock import patch, MagicMock
class ImmoCheckTests(APITestCase):
@patch('properties.services.openai_client.chat.completions.create')
def test_ai_analysis(self, mock_openai):
# Mock the OpenAI response
mock_openai.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content='{"score": 8.5}'))]
)
self.client.force_authenticate(user=self.user)
response = self.client.post('/api/immocheck/', {'property_id': 1})
self.assertEqual(response.status_code, 200)
mock_openai.assert_called_once()
@patch('requests.get')
def test_external_data_fetch(self, mock_get):
mock_get.return_value = MagicMock(
status_code=200,
json=lambda: {'population': 1500000, 'unemployment': 3.2}
)
# Test continues...126. How do you test a ViewSet's custom `@action` endpoint?
class PropertyActionTests(APITestCase):
def test_publish_action(self):
property = Property.objects.create(
title='Draft Property',
owner=self.user,
is_published=False
)
self.client.force_authenticate(user=self.user)
response = self.client.post(f'/api/properties/{property.pk}/publish/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['status'], 'published')
property.refresh_from_db()
self.assertTrue(property.is_published)127. How do you implement API versioning (URL, header, query param)?
# URL versioning (most common)
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
}
# URLs: /api/v1/properties/, /api/v2/properties/
# Header versioning
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
}
# Header: Accept: application/json; version=1.0
# Query param versioning
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.QueryParameterVersioning',
'VERSION_PARAM': 'version',
}
# URL: /api/properties/?version=v2128. What are renderers in DRF and how do you create a custom one?
from rest_framework.renderers import BaseRenderer
import csv
import io
class CSVRenderer(BaseRenderer):
media_type = 'text/csv'
format = 'csv'
def render(self, data, accepted_media_type=None, renderer_context=None):
if not data:
return ''
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
return output.getvalue()
class PropertyExportView(APIView):
renderer_classes = [JSONRenderer, CSVRenderer]
def get(self, request):
properties = Property.objects.all()
serializer = PropertySerializer(properties, many=True)
return Response(serializer.data)
# GET /export/ → JSON
# GET /export/?format=csv → CSV (or Accept: text/csv header)129. What is `JSONRenderer` vs `BrowsableAPIRenderer`?
- JSONRenderer — renders response as application/json. Used by all API clients. - BrowsableAPIRenderer — renders an interactive HTML page. Only for browsers in development.
# Show only in development
import os
if os.environ.get('ENVIRONMENT') == 'production':
DEFAULT_RENDERER_CLASSES = ['rest_framework.renderers.JSONRenderer']
else:
DEFAULT_RENDERER_CLASSES = [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
]130. How do you implement a custom exception handler in DRF?
# utils/exceptions.py
from rest_framework.views import exception_handler
from rest_framework.exceptions import ValidationError, NotFound, PermissionDenied
import logging
logger = logging.getLogger(__name__)
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
error_data = {
'success': False,
'error': {
'status_code': response.status_code,
'detail': response.data,
}
}
# Log server errors
if response.status_code >= 500:
logger.error(f'Server error: {exc}', exc_info=True)
response.data = error_data
return response
# settings.py
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'utils.exceptions.custom_exception_handler'
}131. How do you return standardized error responses across your API?
# Use DRF's built-in exception classes
from rest_framework.exceptions import (
ValidationError, NotFound, PermissionDenied, NotAuthenticated
)
class PropertyView(APIView):
def get(self, request, pk):
try:
property = Property.objects.get(pk=pk)
except Property.DoesNotExist:
raise NotFound({'error': 'Property not found', 'code': 'not_found'})
if not property.is_active:
raise PermissionDenied({'error': 'Property is inactive', 'code': 'inactive'})
return Response(PropertySerializer(property).data)Combined with a custom exception handler, this gives consistent error shapes across all endpoints.
132. How do you handle soft deletes in DRF?
# Model
class Property(models.Model):
is_deleted = models.BooleanField(default=False)
deleted_at = models.DateTimeField(null=True, blank=True)
class Meta:
default_manager_name = 'objects'
# ViewSet
class PropertyViewSet(viewsets.ModelViewSet):
def get_queryset(self):
# Exclude soft-deleted by default
return Property.objects.filter(is_deleted=False)
def perform_destroy(self, instance):
# Override destroy to soft-delete instead
from django.utils import timezone
instance.is_deleted = True
instance.deleted_at = timezone.now()
instance.save()
# Note: returns 204 No Content as expected133. How do you implement a bulk create endpoint?
class BulkPropertySerializer(serializers.ListSerializer):
def create(self, validated_data):
properties = [Property(**item) for item in validated_data]
return Property.objects.bulk_create(properties)
class PropertySerializer(serializers.ModelSerializer):
class Meta:
model = Property
fields = ['title', 'price', 'city']
list_serializer_class = BulkPropertySerializer
class BulkCreateView(APIView):
def post(self, request):
# Accept list of objects
serializer = PropertySerializer(data=request.data, many=True)
if serializer.is_valid():
serializer.save(owner=request.user)
return Response(serializer.data, status=201)
return Response(serializer.errors, status=400)
# POST /api/properties/bulk/
# [{"title": "Flat A", "price": 200000}, {"title": "Flat B", "price": 300000}]135. How do you stream large query results in DRF?
import csv
from django.http import StreamingHttpResponse
class Echo:
def write(self, value):
return value
class PropertyCSVExportView(APIView):
def get(self, request):
def generate_rows():
pseudo_buffer = Echo()
writer = csv.writer(pseudo_buffer)
yield writer.writerow(['ID', 'Title', 'Price', 'City'])
# Stream in chunks — never loads all into memory
for property in Property.objects.filter(is_active=True).iterator(chunk_size=500):
yield writer.writerow([property.id, property.title, property.price, property.city])
response = StreamingHttpResponse(
generate_rows(),
content_type='text/csv'
)
response['Content-Disposition'] = 'attachment; filename="properties.csv"'
return response136. How do you implement an endpoint that returns a file download?
import io
from django.http import FileResponse
from reportlab.pdfgen import canvas
class PropertyPDFView(APIView):
def get(self, request, pk):
property = get_object_or_404(Property, pk=pk)
# Generate PDF in memory
buffer = io.BytesIO()
p = canvas.Canvas(buffer)
p.drawString(100, 750, f"Property: {property.title}")
p.drawString(100, 730, f"Price: €{property.price:,.2f}")
p.showPage()
p.save()
buffer.seek(0)
return FileResponse(
buffer,
as_attachment=True,
filename=f'property_{pk}.pdf',
content_type='application/pdf'
)137. How do you use DRF with Celery for async task triggering?
# tasks.py
from celery import shared_task
@shared_task
def generate_property_analysis(property_id, user_id):
property = Property.objects.get(pk=property_id)
analysis = call_openai_api(property)
PropertyAnalysis.objects.create(property=property, data=analysis)
# views.py
class StartAnalysisView(APIView):
def post(self, request, pk):
property = get_object_or_404(Property, pk=pk, owner=request.user)
# Queue async task — returns immediately
task = generate_property_analysis.delay(pk, request.user.id)
return Response({
'task_id': task.id,
'status': 'queued',
'status_url': f'/api/tasks/{task.id}/'
}, status=202) # 202 Accepted
class TaskStatusView(APIView):
def get(self, request, task_id):
from celery.result import AsyncResult
result = AsyncResult(task_id)
return Response({
'task_id': task_id,
'status': result.status,
'result': result.result if result.ready() else None,
})138. How do you document your DRF API with `drf-spectacular`?
pip install drf-spectacular# settings.py
INSTALLED_APPS = [..., 'drf_spectacular']
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': 'MyApp API',
'DESCRIPTION': 'Property management SaaS API',
'VERSION': '1.0.0',
}
# urls.py
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
urlpatterns = [
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema')),
]# Annotate views for better docs
from drf_spectacular.utils import extend_schema, OpenApiParameter
class PropertyViewSet(viewsets.ModelViewSet):
@extend_schema(
summary="List all properties",
parameters=[
OpenApiParameter('city', str, description='Filter by city'),
OpenApiParameter('min_price', int, description='Minimum price'),
],
responses={200: PropertySerializer(many=True)}
)
def list(self, request):
return super().list(request)139. How do you implement cursor-based pagination for a real-time feed?
from rest_framework.pagination import CursorPagination
class ActivityFeedPagination(CursorPagination):
page_size = 20
ordering = '-created_at' # Must be a stable, unique field
class PropertyActivityViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = ActivitySerializer
pagination_class = ActivityFeedPagination
def get_queryset(self):
return Activity.objects.filter(
property__owner=self.request.user
).select_related('property', 'user')
# Response:
# {
# "next": "http://api/activities/?cursor=cD0yMDIz...",
# "previous": null,
# "results": [...]
# }
# Cursor is an encoded position — no skip/repeat as new items are added140. How does DRF work with Django channels for WebSocket APIs?
# consumers.py (Django Channels)
from channels.generic.websocket import AsyncWebsocketConsumer
from rest_framework.authtoken.models import Token
import json
class PropertyUpdatesConsumer(AsyncWebsocketConsumer):
async def connect(self):
# Authenticate via token in query string
token_key = self.scope['query_string'].decode().split('=')[1]
try:
token = await Token.objects.aget(key=token_key)
self.user = await token.auser()
except Token.DoesNotExist:
await self.close()
return
self.group_name = f'user_{self.user.id}_properties'
await self.channel_layer.group_add(self.group_name, self.channel_name)
await self.accept()
async def property_update(self, event):
await self.send(text_data=json.dumps(event['data']))
# Sending updates from DRF views:
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
class PropertyViewSet(viewsets.ModelViewSet):
def perform_update(self, serializer):
instance = serializer.save()
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
f'user_{instance.owner.id}_properties',
{'type': 'property.update', 'data': PropertySerializer(instance).data}
)141. How do you implement API rate limiting per plan tier?
from rest_framework.throttling import UserRateThrottle
class FreeTierThrottle(UserRateThrottle):
scope = 'free'
class ProTierThrottle(UserRateThrottle):
scope = 'pro'
class EnterpriseTierThrottle(UserRateThrottle):
scope = 'enterprise'
class TierBasedThrottle(UserRateThrottle):
def get_cache_key(self, request, view):
if not request.user.is_authenticated:
return None # Let AnonRateThrottle handle this
return f'throttle_user_{request.user.pk}'
def allow_request(self, request, view):
if not request.user.is_authenticated:
return True
plan = getattr(request.user, 'subscription_plan', 'free')
rates = {'free': '10/day', 'pro': '1000/day', 'enterprise': '100000/day'}
self.rate = rates.get(plan, '10/day')
self.num_requests, self.duration = self.parse_rate(self.rate)
return super().allow_request(request, view)142. What are the common DRF anti-patterns to avoid?
# ❌ Anti-pattern 1: Logic in views instead of serializers
class PropertyView(APIView):
def post(self, request):
title = request.data.get('title', '').strip()
if len(title) < 3:
return Response({'error': 'Title too short'}, status=400)
# ... more manual validation
# ✓ Put validation in serializers where it belongs
# ❌ Anti-pattern 2: N+1 queries
class PropertyViewSet(viewsets.ModelViewSet):
queryset = Property.objects.all() # No select_related → N+1
# ✓ Use select_related/prefetch_related
# ❌ Anti-pattern 3: Inconsistent error responses
return Response({'msg': 'Error'}, status=400) # Some views
return Response({'detail': 'Error'}, status=400) # Other views
# ✓ Use a custom exception handler for consistency
# ❌ Anti-pattern 4: Ignoring object-level permissions
class PropertyView(APIView):
permission_classes = [IsAuthenticated]
def put(self, request, pk):
property = Property.objects.get(pk=pk)
# Missing: is request.user the owner?
# ✓ Always check object-level permissions143. How do you implement a health check endpoint?
from django.db import connection
from django.core.cache import cache
import redis
class HealthCheckView(APIView):
permission_classes = [AllowAny]
authentication_classes = []
def get(self, request):
health = {'status': 'ok', 'checks': {}}
# Database
try:
connection.ensure_connection()
health['checks']['database'] = 'ok'
except Exception as e:
health['checks']['database'] = f'error: {str(e)}'
health['status'] = 'degraded'
# Cache (Redis)
try:
cache.set('health_check', '1', timeout=5)
cache.get('health_check')
health['checks']['cache'] = 'ok'
except Exception as e:
health['checks']['cache'] = f'error: {str(e)}'
health['status'] = 'degraded'
status_code = 200 if health['status'] == 'ok' else 503
return Response(health, status=status_code)144. How do you implement multi-language support in DRF error messages?
# DRF uses Django's i18n system for built-in error messages
# settings.py
USE_I18N = True
LANGUAGE_CODE = 'de' # German default
# For custom messages:
from django.utils.translation import gettext_lazy as _
class PropertySerializer(serializers.ModelSerializer):
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError(_('Der Preis muss positiv sein.'))
return value
# In request: Accept-Language: de
# DRF automatically uses the right language for built-in errors145. What is the difference between `Response` and `HttpResponse` in DRF?
from django.http import HttpResponse # Django's basic response
from rest_framework.response import Response # DRF's response
# HttpResponse — static, no content negotiation
def my_view(request):
return HttpResponse(json.dumps({'key': 'value'}), content_type='application/json')
# Always returns JSON, ignores Accept header
# Response — smart, content-negotiated
class MyView(APIView):
def get(self, request):
return Response({'key': 'value'})
# Returns JSON, HTML, XML etc. based on Accept header and renderer classes
# Data is NOT serialized yet at this point — happens later146. How do you handle conditional requests (ETags, Last-Modified)?
from django.utils.http import http_date
import hashlib
class PropertyDetailView(APIView):
def get(self, request, pk):
property = get_object_or_404(Property, pk=pk)
# Generate ETag from content hash
data = PropertySerializer(property).data
etag = hashlib.md5(str(data).encode()).hexdigest()
last_modified = property.updated_at
# Check If-None-Match
if request.META.get('HTTP_IF_NONE_MATCH') == f'"{etag}"':
return Response(status=304)
response = Response(data)
response['ETag'] = f'"{etag}"'
response['Last-Modified'] = http_date(last_modified.timestamp())
return response147. How do you cache API responses in DRF?
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
class PropertyListView(generics.ListAPIView):
@method_decorator(cache_page(60 * 15)) # Cache 15 minutes
@method_decorator(vary_on_headers('Authorization')) # Different cache per user
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)Or manually with Redis:
from django.core.cache import cache
class CityStatsView(APIView):
def get(self, request, city):
cache_key = f'city_stats_{city}'
data = cache.get(cache_key)
if data is None:
data = compute_expensive_city_stats(city)
cache.set(cache_key, data, timeout=3600) # 1 hour
return Response(data)148. How do you implement request logging in DRF?
# middleware/logging.py
import time
import logging
logger = logging.getLogger('api')
class APILoggingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start = time.time()
response = self.get_response(request)
duration = time.time() - start
if request.path.startswith('/api/'):
logger.info(
f'{request.method} {request.path} '
f'status={response.status_code} '
f'duration={duration:.3f}s '
f'user={getattr(request.user, "id", "anon")}'
)
return response
# settings.py
MIDDLEWARE = [
'middleware.logging.APILoggingMiddleware',
...
]149. How do you implement HATEOAS (hypermedia links) in DRF?
HATEOAS means responses include links to related actions/resources:
class PropertySerializer(serializers.ModelSerializer):
links = serializers.SerializerMethodField()
class Meta:
model = Property
fields = ['id', 'title', 'price', 'links']
def get_links(self, obj):
request = self.context.get('request')
return {
'self': request.build_absolute_uri(f'/api/properties/{obj.pk}/'),
'owner': request.build_absolute_uri(f'/api/users/{obj.owner_id}/'),
'images': request.build_absolute_uri(f'/api/properties/{obj.pk}/images/'),
'publish': request.build_absolute_uri(f'/api/properties/{obj.pk}/publish/'),
}
# Response: {"id": 1, "title": "...", "links": {"self": "http://...", ...}}This guide was designed for developers who want to build a deep, lasting understanding of Django REST Framework — not just pass an interview. Every question here is something that comes up in real production systems.
