Django Interview Questions: ORM, Middleware and DRF Deep Dive

Django interview questions covering ORM optimization with select_related and prefetch_related, middleware architecture, and Django REST Framework serializer performance, permissions, and pagination patterns.

Django interview preparation covering ORM, Middleware, and Django REST Framework concepts

Django interview questions cover three pillars that separate senior candidates from the rest: ORM mastery, middleware architecture, and Django REST Framework (DRF) design patterns. This guide breaks down the exact questions hiring managers ask in 2026, with production-ready code examples using Django 5.2 LTS and DRF 3.17.

What interviewers actually test

Django interviews rarely ask about basic CRUD. The focus has shifted to QuerySet optimization (select_related vs prefetch_related), custom middleware design, and DRF serializer performance. Candidates who can explain N+1 queries and write efficient viewsets consistently outperform those who only know generic views.

Django ORM Interview Questions: QuerySet Optimization

The most common Django ORM interview question targets the N+1 problem. Given a model with foreign key relationships, candidates must demonstrate when to use select_related versus prefetch_related.

python
# models.py
from django.db import models

class Company(models.Model):
    name = models.CharField(max_length=200)
    founded_year = models.IntegerField()

class Developer(models.Model):
    name = models.CharField(max_length=200)
    company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="developers")
    skills = models.ManyToManyField("Skill", related_name="developers")

class Skill(models.Model):
    name = models.CharField(max_length=100)
    category = models.CharField(max_length=50)

The difference between the two methods comes down to the type of relationship being traversed.

python
# queries.py — Correct approach for ForeignKey (single object)
developers = Developer.objects.select_related("company").all()
# Generates ONE SQL query with JOIN
# SELECT developer.*, company.* FROM developer INNER JOIN company ...

# Correct approach for ManyToMany (multiple objects)
developers = Developer.objects.prefetch_related("skills").all()
# Generates TWO SQL queries:
# 1. SELECT * FROM developer
# 2. SELECT * FROM skill INNER JOIN developer_skills WHERE developer_id IN (...)

select_related performs a SQL JOIN and works with ForeignKey and OneToOneField. prefetch_related executes a separate query and works with ManyToManyField and reverse ForeignKey relations. Mixing them up causes either unnecessary JOINs on large datasets or the dreaded N+1 pattern.

Advanced ORM: Custom Managers and QuerySet Chaining

Interviewers often ask candidates to write a custom manager that encapsulates business logic. The goal is to verify that the candidate understands Django's manager pattern and can write reusable query interfaces.

python
# managers.py
from django.db import models
from django.utils import timezone

class ActiveDeveloperQuerySet(models.QuerySet):
    def active(self):
        """Filter developers who logged in within the last 30 days."""
        cutoff = timezone.now() - timezone.timedelta(days=30)
        return self.filter(last_login__gte=cutoff)

    def senior(self):
        """Filter developers with 5+ years of experience."""
        return self.filter(years_experience__gte=5)

    def by_skill(self, skill_name):
        """Filter developers by a specific skill."""
        return self.filter(skills__name=skill_name)

class ActiveDeveloperManager(models.Manager):
    def get_queryset(self):
        return ActiveDeveloperQuerySet(self.model, using=self._db)

    def active(self):
        return self.get_queryset().active()

The key follow-up question: "Why use a custom QuerySet instead of just a Manager?" The answer is chaining. Custom QuerySet methods can be chained together, while Manager methods cannot be chained after the first call.

python
# usage.py — QuerySet chaining in action
# This works because each method returns a QuerySet
senior_python_devs = (
    Developer.active_objects  # custom manager
    .active()                 # ActiveDeveloperQuerySet method
    .senior()                 # chains another QuerySet method
    .by_skill("Python")       # chains a third method
    .select_related("company")  # standard QuerySet method still works
)
Django 5.2 LTS: Composite Primary Keys

Django 5.2 introduced CompositePrimaryKey, a long-awaited feature for models that need multi-column primary keys. Interview questions about this feature are becoming more common, especially for candidates working with legacy databases or data warehouse schemas.

Django Middleware Interview Questions: Request-Response Pipeline

Middleware questions test a candidate's understanding of Django's request-response lifecycle. The standard question: "Explain the order in which middleware processes a request and a response."

The answer follows a strict pattern. During a request, middleware classes execute top-to-bottom as defined in MIDDLEWARE. During a response, they execute bottom-to-top. This onion-layer architecture means the first middleware in the list wraps everything.

python
# middleware.py
import time
import logging
from django.http import JsonResponse

logger = logging.getLogger(__name__)

class RequestTimingMiddleware:
    """Logs the time taken to process each request."""

    def __init__(self, get_response):
        self.get_response = get_response  # next middleware or view

    def __call__(self, request):
        start_time = time.monotonic()

        response = self.get_response(request)  # passes to next layer

        duration_ms = (time.monotonic() - start_time) * 1000
        logger.info(
            "method=%s path=%s status=%d duration=%.2fms",
            request.method,
            request.path,
            response.status_code,
            duration_ms,
        )
        return response

A common follow-up: "How would you short-circuit the middleware chain?" Returning an HttpResponse from __call__ before calling self.get_response(request) stops the chain entirely. The remaining middleware and the view never execute.

python
# middleware.py — Rate limiting with short-circuit
from django.core.cache import cache

class RateLimitMiddleware:
    """Blocks requests exceeding 100 per minute per IP."""

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        ip = request.META.get("REMOTE_ADDR")
        cache_key = f"rate_limit:{ip}"
        request_count = cache.get(cache_key, 0)

        if request_count >= 100:
            return JsonResponse(  # short-circuits — view never executes
                {"error": "Rate limit exceeded. Try again in 60 seconds."},
                status=429,
            )

        cache.set(cache_key, request_count + 1, timeout=60)
        return self.get_response(request)

Middleware Hooks: process_view, process_exception, and process_template_response

Beyond __call__, Django middleware supports three optional hook methods. Interviewers use these to gauge depth of knowledge.

python
# middleware.py — Full middleware with all hooks
class AuditMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_view(self, request, view_func, view_args, view_kwargs):
        """Called after URL resolution, before the view executes."""
        logger.info("Calling view: %s", view_func.__name__)
        return None  # returning None continues normal processing

    def process_exception(self, request, exception):
        """Called only if the view raises an exception."""
        logger.error("View exception: %s", exception, exc_info=True)
        return None  # returning None lets Django's default handling proceed

    def process_template_response(self, request, response):
        """Called if the response has a render() method (TemplateResponse)."""
        response.context_data["audit_timestamp"] = time.time()
        return response  # must return a response with render()

process_view fires after URL resolution but before the view. Returning None continues execution; returning an HttpResponse short-circuits. process_exception fires only on unhandled exceptions. process_template_response fires only for TemplateResponse objects, not regular HttpResponse.

Ready to ace your Django interviews?

Practice with our interactive simulators, flashcards, and technical tests.

DRF Interview Questions: Serializer Performance

Django REST Framework serializer questions focus on nested serialization and the performance implications of different approaches. The most asked question: "How do you handle nested relationships without causing N+1 queries?"

python
# serializers.py
from rest_framework import serializers
from .models import Developer, Company, Skill

class SkillSerializer(serializers.ModelSerializer):
    class Meta:
        model = Skill
        fields = ["name", "category"]

class CompanySerializer(serializers.ModelSerializer):
    class Meta:
        model = Company
        fields = ["name", "founded_year"]

class DeveloperSerializer(serializers.ModelSerializer):
    company = CompanySerializer(read_only=True)   # nested FK
    skills = SkillSerializer(many=True, read_only=True)  # nested M2M

    class Meta:
        model = Developer
        fields = ["id", "name", "company", "skills"]

The serializer alone does not solve performance. The ViewSet must optimize the queryset.

python
# views.py
from rest_framework import viewsets
from .models import Developer
from .serializers import DeveloperSerializer

class DeveloperViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = DeveloperSerializer

    def get_queryset(self):
        return (
            Developer.objects
            .select_related("company")       # JOIN for FK
            .prefetch_related("skills")      # separate query for M2M
            .order_by("-id")
        )

Without select_related and prefetch_related in the ViewSet, each serialized developer triggers individual queries for its company and skills. On a list of 50 developers, that means 1 + 50 + 50 = 101 queries instead of 3.

DRF Custom Permissions and Authentication

A frequent DRF interview question: "Write a custom permission that restricts access based on the user's role and the object's owner."

python
# permissions.py
from rest_framework.permissions import BasePermission

class IsOwnerOrAdmin(BasePermission):
    """
    Object-level permission:
    - Admin users can access any object
    - Regular users can only access objects they own
    """
    message = "Access restricted to the object owner or admin users."

    def has_object_permission(self, request, view, obj):
        if request.user.is_staff:
            return True
        # Assumes the model has an 'owner' field pointing to User
        return obj.owner == request.user
python
# views.py — Applying custom permissions
from rest_framework import viewsets, permissions
from .permissions import IsOwnerOrAdmin

class ProjectViewSet(viewsets.ModelViewSet):
    permission_classes = [permissions.IsAuthenticated, IsOwnerOrAdmin]

    def get_queryset(self):
        # Non-admin users only see their own projects
        if self.request.user.is_staff:
            return Project.objects.all()
        return Project.objects.filter(owner=self.request.user)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)  # auto-assign owner

The nuance interviewers look for: has_permission runs on every request (list-level), while has_object_permission runs only when get_object() is called (detail-level). Forgetting to override get_queryset for list views creates a security gap where users can see objects they do not own.

Common DRF security mistake

Relying solely on has_object_permission without filtering the queryset leaves the list endpoint unprotected. Always combine object-level permissions with a filtered get_queryset to enforce access control on both list and detail views.

DRF Throttling and Pagination Patterns

Throttling and pagination are standard follow-up questions. Interviewers want to see that candidates understand production-ready API configuration.

python
# settings.py — Production DRF configuration
REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "20/minute",    # unauthenticated users
        "user": "200/minute",   # authenticated users
    },
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.CursorPagination",
    "PAGE_SIZE": 25,
}
python
# pagination.py — Custom cursor pagination for consistent ordering
from rest_framework.pagination import CursorPagination

class CreatedAtCursorPagination(CursorPagination):
    page_size = 25
    ordering = "-created_at"       # must be a unique, sequential field
    cursor_query_param = "cursor"  # ?cursor=abc123

Cursor-based pagination outperforms offset pagination on large datasets because it does not require counting total rows. The trade-off: clients cannot jump to arbitrary pages. This is the expected answer when interviewers ask "Why would you choose cursor pagination over page number pagination?"

Conclusion

Key takeaways for Django interview preparation:

  • ORM optimization: Always match the query method to the relationship type. select_related for ForeignKey/OneToOne, prefetch_related for ManyToMany and reverse FK. Custom QuerySets enable chainable, reusable business logic.
  • Middleware architecture: The request flows top-to-bottom, the response bottom-to-top. Short-circuiting by returning a response before get_response() is a fundamental pattern for rate limiting, auth checks, and request validation.
  • DRF serializer performance: Nested serializers require explicit queryset optimization in the ViewSet. Without select_related/prefetch_related, serialization causes N+1 queries at scale.
  • DRF permissions: Combine has_object_permission with a filtered get_queryset to enforce access control on both list and detail endpoints. Object-level permissions alone leave list views unprotected.
  • Throttling and pagination: Cursor pagination scales better than offset on large tables. Configure throttle rates separately for anonymous and authenticated users.

Practice these patterns with Django ORM interview questions and Django middleware questions on SharpSkill to build confidence before the real interview.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Tags

#django
#python
#interview
#orm
#middleware
#drf
#rest-api

Share

Related articles