Understanding Django's request-response cycle is fundamental to building effective web applications. This comprehensive guide explores every step of how Django processes HTTP requests, from the initial URL resolution to the final response delivery.
Django's request handling follows a well-defined pipeline that ensures security, flexibility, and maintainability. Here's the complete flow:
1. Web Server (nginx/Apache) → 2. WSGI/ASGI Server → 3. Django Application
↓
4. URL Dispatcher → 5. Middleware (Request) → 6. View Function/Class
↓
7. Model Layer → 8. Database → 9. Template Engine → 10. Middleware (Response)
↓
11. HTTP Response → 12. Web Server → 13. Client Browser
When a user visits your Django application, the web server (nginx, Apache, or Django's development server) receives the HTTP request.
Example HTTP Request:
GET /blog/posts/123/ HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html,application/xhtml+xml
Cookie: sessionid=abc123; csrftoken=xyz789
The web server passes the request to Django through the WSGI (Web Server Gateway Interface) or ASGI (Asynchronous Server Gateway Interface) protocol.
WSGI Configuration (wsgi.py):
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
application = get_wsgi_application()
ASGI Configuration (asgi.py):
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
application = get_asgi_application()
Django converts the raw HTTP request into an HttpRequest object containing all request data:
# Django automatically creates this object
class HttpRequest:
def __init__(self):
self.method = 'GET' # HTTP method
self.path = '/blog/posts/123/' # URL path
self.GET = QueryDict() # GET parameters
self.POST = QueryDict() # POST data
self.COOKIES = {} # Cookies
self.META = {} # HTTP headers and server info
self.FILES = {} # Uploaded files
self.user = AnonymousUser() # User object (set by auth middleware)
Accessing Request Data:
def my_view(request):
# HTTP method
method = request.method # 'GET', 'POST', etc.
# URL information
path = request.path # '/blog/posts/123/'
full_path = request.get_full_path() # '/blog/posts/123/?page=1'
# Query parameters
page = request.GET.get('page', 1)
search = request.GET.get('q', '')
# POST data
title = request.POST.get('title', '')
# Headers
user_agent = request.META.get('HTTP_USER_AGENT')
remote_ip = request.META.get('REMOTE_ADDR')
# Cookies
session_id = request.COOKIES.get('sessionid')
# Files
uploaded_file = request.FILES.get('document')
# User (set by authentication middleware)
current_user = request.user
Django's URL dispatcher matches the request path against URL patterns defined in urls.py files.
URL Resolution Process:
# myproject/urls.py (Root URLconf)
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls')), # Delegate to app URLs
path('api/', include('api.urls')),
]
# blog/urls.py (App URLconf)
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.PostListView.as_view(), name='post_list'),
path('posts/<int:pk>/', views.post_detail, name='post_detail'),
path('posts/<slug:slug>/', views.post_by_slug, name='post_by_slug'),
path('category/<str:category>/', views.posts_by_category, name='category_posts'),
]
URL Pattern Matching:
# For request: /blog/posts/123/
# Django matches: path('posts/<int:pk>/', views.post_detail, name='post_detail')
# Extracted parameters: pk=123
# View function called: views.post_detail(request, pk=123)
# For request: /blog/posts/my-first-post/
# Django matches: path('posts/<slug:slug>/', views.post_by_slug, name='post_by_slug')
# Extracted parameters: slug='my-first-post'
# View function called: views.post_by_slug(request, slug='my-first-post')
URL Pattern Types:
from django.urls import path, re_path
urlpatterns = [
# String parameter (default)
path('posts/<str:slug>/', views.post_by_slug),
# Integer parameter
path('posts/<int:pk>/', views.post_detail),
# Slug parameter (letters, numbers, hyphens, underscores)
path('category/<slug:category>/', views.category_posts),
# UUID parameter
path('user/<uuid:user_id>/', views.user_profile),
# Path parameter (includes slashes)
path('files/<path:file_path>/', views.serve_file),
# Regular expression patterns
re_path(r'^posts/(?P<year>[0-9]{4})/$', views.posts_by_year),
re_path(r'^posts/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.posts_by_month),
]
Before reaching the view, the request passes through middleware in the order defined in MIDDLEWARE setting.
Default Django Middleware:
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # Security headers
'django.contrib.sessions.middleware.SessionMiddleware', # Session handling
'django.middleware.common.CommonMiddleware', # Common functionality
'django.middleware.csrf.CsrfViewMiddleware', # CSRF protection
'django.contrib.auth.middleware.AuthenticationMiddleware', # User authentication
'django.contrib.messages.middleware.MessageMiddleware', # Message framework
'django.middleware.clickjacking.XFrameOptionsMiddleware', # Clickjacking protection
]
Custom Middleware Example:
# middleware.py
import time
import logging
logger = logging.getLogger(__name__)
class RequestLoggingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Process request before view
start_time = time.time()
# Log request details
logger.info(f"Request: {request.method} {request.path}")
# Add custom data to request
request.start_time = start_time
request.custom_header = request.META.get('HTTP_X_CUSTOM_HEADER')
# Call the next middleware or view
response = self.get_response(request)
# Process response after view
duration = time.time() - start_time
logger.info(f"Response: {response.status_code} ({duration:.2f}s)")
# Add custom headers to response
response['X-Response-Time'] = f"{duration:.2f}s"
return response
def process_exception(self, request, exception):
"""Called when a view raises an exception"""
logger.error(f"Exception in {request.path}: {exception}")
return None # Let Django handle the exception
Middleware Registration:
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'myapp.middleware.RequestLoggingMiddleware', # Custom middleware
'django.contrib.sessions.middleware.SessionMiddleware',
# ... other middleware
]
The view function or class processes the request and returns an HTTP response.
Function-Based View:
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, JsonResponse
from django.views.decorators.http import require_http_methods
from django.views.decorators.cache import cache_page
from .models import Post
@require_http_methods(["GET", "POST"])
@cache_page(60 * 15) # Cache for 15 minutes
def post_detail(request, pk):
"""Display a single blog post"""
post = get_object_or_404(Post, pk=pk, published=True)
# Handle different request methods
if request.method == 'POST':
# Process form submission
comment_text = request.POST.get('comment')
if comment_text:
Comment.objects.create(
post=post,
author=request.user,
text=comment_text
)
return redirect('blog:post_detail', pk=pk)
# Prepare context data
context = {
'post': post,
'comments': post.comments.filter(approved=True),
'related_posts': Post.objects.filter(
category=post.category
).exclude(pk=pk)[:3],
'user_can_comment': request.user.is_authenticated,
}
# Return different responses based on request
if request.headers.get('Accept') == 'application/json':
# API response
data = {
'title': post.title,
'content': post.content,
'author': post.author.username,
'created_at': post.created_at.isoformat(),
}
return JsonResponse(data)
# HTML response
return render(request, 'blog/post_detail.html', context)
Class-Based View:
from django.views.generic import DetailView, ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_queryset(self):
"""Filter queryset based on user permissions"""
queryset = super().get_queryset()
if not self.request.user.is_staff:
queryset = queryset.filter(published=True)
return queryset
def get_context_data(self, **kwargs):
"""Add extra context data"""
context = super().get_context_data(**kwargs)
context['comments'] = self.object.comments.filter(approved=True)
context['related_posts'] = Post.objects.filter(
category=self.object.category
).exclude(pk=self.object.pk)[:3]
return context
def render_to_response(self, context, **response_kwargs):
"""Customize response based on request"""
if self.request.headers.get('Accept') == 'application/json':
data = {
'title': context['post'].title,
'content': context['post'].content,
'author': context['post'].author.username,
}
return JsonResponse(data)
return super().render_to_response(context, **response_kwargs)
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
"""Filter and order posts"""
queryset = Post.objects.filter(published=True)
# Handle search
search_query = self.request.GET.get('q')
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query) |
Q(content__icontains=search_query)
)
# Handle category filter
category = self.request.GET.get('category')
if category:
queryset = queryset.filter(category__slug=category)
return queryset.order_by('-created_at')
Views interact with models to retrieve, create, update, or delete data.
Database Query Examples:
from django.db.models import Q, Count, Avg
from .models import Post, Comment
def blog_statistics(request):
"""View demonstrating various database operations"""
# Simple queries
all_posts = Post.objects.all()
published_posts = Post.objects.filter(published=True)
recent_posts = Post.objects.filter(
created_at__gte=timezone.now() - timedelta(days=7)
)
# Complex queries with Q objects
search_posts = Post.objects.filter(
Q(title__icontains='django') | Q(content__icontains='python')
)
# Aggregation
stats = Post.objects.aggregate(
total_posts=Count('id'),
avg_comments=Avg('comments__count'),
latest_post=Max('created_at')
)
# Annotations
posts_with_comment_count = Post.objects.annotate(
comment_count=Count('comments')
).filter(comment_count__gt=5)
# Optimization with select_related and prefetch_related
optimized_posts = Post.objects.select_related(
'author', 'category'
).prefetch_related(
'comments__author', 'tags'
)
# Raw SQL (when needed)
posts_raw = Post.objects.raw(
"SELECT * FROM blog_post WHERE created_at > %s",
[timezone.now() - timedelta(days=30)]
)
context = {
'stats': stats,
'recent_posts': recent_posts,
'popular_posts': posts_with_comment_count,
}
return render(request, 'blog/statistics.html', context)
Django's template engine processes templates with context data to generate HTML.
Template Processing:
# View passes context to template
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
context = {
'post': post,
'user': request.user,
'current_time': timezone.now(),
}
return render(request, 'blog/post_detail.html', context)
Template Example:
<!-- blog/post_detail.html -->
{% extends 'base.html' %}
{% load static %}
{% load custom_tags %}
{% block title %}{{ post.title }} - {{ block.super }}{% endblock %}
{% block content %}
<article class="post-detail">
<header>
<h1>{{ post.title }}</h1>
<div class="post-meta">
<span>By {{ post.author.get_full_name|default:post.author.username }}</span>
<time datetime="{{ post.created_at|date:'c' }}">
{{ post.created_at|date:"F d, Y" }}
</time>
{% if post.updated_at != post.created_at %}
<span class="updated">
(Updated: {{ post.updated_at|date:"M d, Y" }})
</span>
{% endif %}
</div>
</header>
<div class="post-content">
{{ post.content|linebreaks|safe }}
</div>
{% if post.featured_image %}
<figure class="featured-image">
<img src="{{ post.featured_image.url }}" alt="{{ post.title }}">
</figure>
{% endif %}
<footer class="post-footer">
<div class="tags">
{% for tag in post.tags.all %}
<a href="{% url 'blog:tag_posts' tag.slug %}" class="tag">
{{ tag.name }}
</a>
{% endfor %}
</div>
{% if user.is_authenticated %}
<div class="post-actions">
{% if user == post.author %}
<a href="{% url 'blog:post_edit' post.pk %}">Edit</a>
{% endif %}
<a href="{% url 'blog:post_like' post.pk %}">
{% if user in post.likes.all %}Unlike{% else %}Like{% endif %}
</a>
</div>
{% endif %}
</footer>
</article>
<!-- Comments section -->
<section class="comments">
<h3>Comments ({{ post.comments.count }})</h3>
{% for comment in post.comments.all %}
<div class="comment">
<div class="comment-meta">
<strong>{{ comment.author.username }}</strong>
<time>{{ comment.created_at|timesince }} ago</time>
</div>
<div class="comment-content">
{{ comment.content|linebreaks }}
</div>
</div>
{% empty %}
<p>No comments yet.</p>
{% endfor %}
{% if user.is_authenticated %}
<form method="post" class="comment-form">
{% csrf_token %}
<textarea name="comment" placeholder="Add a comment..."></textarea>
<button type="submit">Post Comment</button>
</form>
{% else %}
<p><a href="{% url 'login' %}">Login</a> to post a comment.</p>
{% endif %}
</section>
{% endblock %}
After the view returns a response, middleware processes it in reverse order.
Response Processing:
class ResponseModificationMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# Modify response headers
response['X-Powered-By'] = 'Django'
response['X-Frame-Options'] = 'DENY'
# Add security headers
if request.is_secure():
response['Strict-Transport-Security'] = 'max-age=31536000'
# Compress response (simplified example)
if 'gzip' in request.META.get('HTTP_ACCEPT_ENCODING', ''):
# Compress response content
pass
return response
Django creates an HttpResponse object containing the final response data.
Response Types:
from django.http import (
HttpResponse, JsonResponse, HttpResponseRedirect,
HttpResponseNotFound, HttpResponseForbidden,
FileResponse, StreamingHttpResponse
)
def various_responses(request):
# HTML response
html_response = HttpResponse('<h1>Hello, World!</h1>', content_type='text/html')
# JSON response
json_response = JsonResponse({'message': 'Hello, World!'})
# Redirect response
redirect_response = HttpResponseRedirect('/blog/')
# File download
file_response = FileResponse(
open('document.pdf', 'rb'),
as_attachment=True,
filename='document.pdf'
)
# Streaming response for large data
def generate_csv():
yield 'Name,Email\n'
for user in User.objects.all():
yield f'{user.username},{user.email}\n'
streaming_response = StreamingHttpResponse(
generate_csv(),
content_type='text/csv'
)
# Custom status codes
not_found_response = HttpResponseNotFound('<h1>Page Not Found</h1>')
forbidden_response = HttpResponseForbidden('<h1>Access Denied</h1>')
return html_response
Complete flow for creating a blog post:
# 1. URL pattern
path('posts/create/', views.PostCreateView.as_view(), name='post_create')
# 2. View
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
fields = ['title', 'content', 'category', 'tags']
template_name = 'blog/post_create.html'
def form_valid(self, form):
# Set the author to current user
form.instance.author = self.request.user
# Generate slug from title
form.instance.slug = slugify(form.instance.title)
# Save the post
response = super().form_valid(form)
# Send notification email
send_mail(
'New Post Created',
f'Your post "{self.object.title}" has been created.',
'noreply@example.com',
[self.request.user.email],
)
return response
def get_success_url(self):
return reverse('blog:post_detail', kwargs={'pk': self.object.pk})
# 3. Template
"""
{% extends 'base.html' %}
{% block content %}
<h2>Create New Post</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Create Post</button>
</form>
{% endblock %}
"""
Handling AJAX requests for dynamic content:
# View
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required
@require_POST
@login_required
def like_post(request, post_id):
"""Handle AJAX post like/unlike"""
try:
post = get_object_or_404(Post, pk=post_id)
if request.user in post.likes.all():
post.likes.remove(request.user)
liked = False
else:
post.likes.add(request.user)
liked = True
return JsonResponse({
'success': True,
'liked': liked,
'like_count': post.likes.count(),
})
except Exception as e:
return JsonResponse({
'success': False,
'error': str(e)
}, status=400)
# JavaScript (in template)
"""
<script>
function toggleLike(postId) {
fetch(`/blog/posts/${postId}/like/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
if (data.success) {
const button = document.getElementById(`like-btn-${postId}`);
button.textContent = data.liked ? 'Unlike' : 'Like';
document.getElementById(`like-count-${postId}`).textContent = data.like_count;
}
});
}
</script>
"""
# views.py
def custom_404(request, exception):
"""Custom 404 error page"""
return render(request, 'errors/404.html', {
'request_path': request.path,
'exception': exception,
}, status=404)
def custom_500(request):
"""Custom 500 error page"""
return render(request, 'errors/500.html', status=500)
# urls.py
handler404 = 'myapp.views.custom_404'
handler500 = 'myapp.views.custom_500'
class ExceptionHandlingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
try:
response = self.get_response(request)
except Exception as e:
# Log the exception
logger.exception(f"Unhandled exception in {request.path}")
# Return custom error response
if request.headers.get('Accept') == 'application/json':
return JsonResponse({
'error': 'Internal server error',
'message': str(e) if settings.DEBUG else 'Something went wrong'
}, status=500)
return render(request, 'errors/500.html', status=500)
return response
# Inefficient: N+1 queries
def inefficient_view(request):
posts = Post.objects.all()
for post in posts:
print(post.author.username) # Database query for each post
print(post.category.name) # Database query for each post
# Efficient: Use select_related
def efficient_view(request):
posts = Post.objects.select_related('author', 'category').all()
for post in posts:
print(post.author.username) # No additional query
print(post.category.name) # No additional query
# For many-to-many relationships
def optimized_view(request):
posts = Post.objects.prefetch_related('tags', 'comments__author').all()
for post in posts:
for tag in post.tags.all(): # No additional queries
print(tag.name)
for comment in post.comments.all(): # No additional queries
print(comment.author.username)
from django.views.decorators.cache import cache_page
from django.core.cache import cache
# View-level caching
@cache_page(60 * 15) # Cache for 15 minutes
def cached_view(request):
expensive_data = perform_expensive_operation()
return render(request, 'template.html', {'data': expensive_data})
# Manual caching
def manual_cache_view(request):
cache_key = f'expensive_data_{request.user.id}'
data = cache.get(cache_key)
if data is None:
data = perform_expensive_operation()
cache.set(cache_key, data, 60 * 15) # Cache for 15 minutes
return render(request, 'template.html', {'data': data})
# CSRF protection is automatic for forms
def form_view(request):
if request.method == 'POST':
# CSRF token is automatically validated
form = MyForm(request.POST)
if form.is_valid():
form.save()
else:
form = MyForm()
return render(request, 'form.html', {'form': form})
# For AJAX requests
from django.views.decorators.csrf import csrf_exempt
from django.middleware.csrf import get_token
def get_csrf_token(request):
"""Provide CSRF token for AJAX requests"""
return JsonResponse({'csrf_token': get_token(request)})
# Exempt view from CSRF (use carefully)
@csrf_exempt
def api_endpoint(request):
# This view doesn't require CSRF token
# Only use for APIs with other authentication
pass
from django.core.exceptions import ValidationError
from django.utils.html import escape
def secure_view(request):
# Always validate and sanitize input
user_input = request.POST.get('content', '')
# Escape HTML to prevent XSS
safe_content = escape(user_input)
# Validate input length
if len(user_input) > 1000:
raise ValidationError('Content too long')
# Use forms for complex validation
form = MyForm(request.POST)
if form.is_valid():
# Form handles validation and sanitization
cleaned_data = form.cleaned_data
return render(request, 'template.html', {'content': safe_content})
# settings.py
if DEBUG:
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': lambda request: True,
}
class DebugMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if settings.DEBUG:
print(f"Request: {request.method} {request.path}")
print(f"User: {request.user}")
print(f"GET params: {dict(request.GET)}")
print(f"POST data: {dict(request.POST)}")
response = self.get_response(request)
if settings.DEBUG:
print(f"Response: {response.status_code}")
print(f"Content-Type: {response.get('Content-Type')}")
return response
# Good: Simple view with clear responsibility
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk, published=True)
return render(request, 'blog/post_detail.html', {'post': post})
# Better: Use class-based views for complex logic
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
def get_queryset(self):
return Post.objects.filter(published=True)
from django.views.decorators.http import require_http_methods
@require_http_methods(["GET", "POST"])
def contact_view(request):
if request.method == 'POST':
# Handle form submission
pass
else:
# Display form
pass
def robust_view(request):
try:
data = expensive_operation()
except ExternalServiceError:
messages.error(request, 'Service temporarily unavailable')
return redirect('home')
except ValidationError as e:
messages.error(request, str(e))
return redirect('form_page')
return render(request, 'template.html', {'data': data})
def api_view(request):
if request.headers.get('Accept') == 'application/json':
return JsonResponse({'data': 'json_data'})
else:
return render(request, 'template.html', {'data': 'html_data'})
Understanding Django's request-response cycle enables you to build efficient, secure, and maintainable web applications. This knowledge helps you debug issues, optimize performance, and implement custom functionality that integrates seamlessly with Django's architecture.
Django Quick Start Guide
This guide gets you from zero to a working Django application in under 30 minutes. Perfect for developers who want to see Django in action quickly or need a rapid refresher on Django fundamentals.
Django Settings
Django settings are the configuration backbone of your Django project. They control everything from database connections to security features, middleware configuration, and application behavior. Understanding how to properly configure and manage settings is crucial for building robust Django applications.