# 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. - Published: 2026-04-03 - Updated: 2026-04-03 - Author: SharpSkill - Tags: django, python, interview, orm, middleware, drf, rest-api - Reading time: 9 min --- 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`. ## DRF Interview Questions: Serializer Performance [Django REST Framework](https://www.django-rest-framework.org/) 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](/technologies/django/interview-questions/django-orm-advanced) and [Django middleware questions](/technologies/django/interview-questions/django-middleware) on SharpSkill to build confidence before the real interview. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/django/django-interview-orm-middleware-drf