The definitive guide for backend engineers: when to use FBVs, when to reach for CBVs, how to build custom mixin chains, and how production teams ship views that are fast, secure, and maintainable at scale.
Orginally Posted in :https://alansomathewdev.blogspot.com/2026/04/django-views-function-based-vs-class.html
Table of Contents
- Introduction
- Why This Matters in Production Systems
- Core Concepts
- Architecture Design
- Step-by-Step Implementation
- Code Examples
- Performance Optimization
- Security Best Practices
- Common Developer Mistakes
- Real Production Use Cases
- Conclusion
Introduction
Every Django view has the same job: accept an HTTP request, do some work, and return an HTTP response. Simple. Yet Django provides two fundamentally different ways to build views — and the choice between them is one of the most debated topics in the Django community.
Function-Based Views (FBVs) are plain Python functions. You can read one from top to bottom and understand exactly what it does. No inheritance chains. No method resolution order. No magic class attributes. What you see is what you get.
Class-Based Views (CBVs) are Python classes that inherit from Django's base View or any of its generic subclasses (ListView, CreateView, UpdateView, etc.). They leverage object-oriented programming — inheritance, mixins, method overriding — to eliminate boilerplate, centralise cross-cutting concerns, and build deeply composable view behaviours.
Both are valid. Both are actively used in production. The engineers who write good Django code don't pick one and reject the other — they understand the strengths of each and choose deliberately.
This guide will give you that understanding. We'll cover the internal mechanics of both approaches, build production-grade examples, trace through mixin chains, apply authentication and rate limiting, optimise for high-traffic endpoints, and show you the patterns used in real systems.
By the end, you'll know exactly when to write a function and when to write a class — and why.
Why This Matters in Production Systems
The view layer is where HTTP meets your application logic. It's not just a router — it's the surface area that determines:
- Security posture: Are authentication checks enforced consistently across all endpoints? Is object-level ownership verified? Are rate limits applied?
- Testability: Can you test the business logic without an HTTP client? Can you test authentication independently of the view?
- Maintainability: When you add a new requirement (e.g., "all API views must log requests"), do you update one place or fifty?
- Performance: Are views triggering N+1 queries? Are they hitting the cache first? Are expensive operations deferred?
FBVs and CBVs make different tradeoffs on all of these dimensions. In a small application with five views, the choice barely matters. In a production application with fifty engineers and five hundred views, it matters enormously.
Django initially only supported function-based views, but they were hard to extend, didn't take advantage of object-oriented programming (OOP) principles, and weren't DRY. This is why Django developers decided to add support for class-based views, which were introduced in Django 1.3.
The practical reality in 2025 production systems: most mature Django codebases use both — FBVs for simple, one-off endpoints where clarity matters, CBVs and generic views for CRUD operations and API resources where DRY matters more. The art is in knowing which to reach for.
Core Concepts
Function-Based Views (FBVs)
A function-based view is a Python callable that accepts an HttpRequest as its first argument and returns an HttpResponse. That's the complete contract.
# The minimal FBV
from django.http import HttpRequest, HttpResponse
def hello(request: HttpRequest) -> HttpResponse:
return HttpResponse("Hello, world")
Everything else — authentication, query logic, template rendering, form handling — you write explicitly. The control flow is linear and readable.
FBV strengths:
- Explicit, top-to-bottom control flow — anyone can read it
- Easy to apply decorators (
@login_required,@require_POST) - Zero learning curve — it's just a function
- Easy to test with
RequestFactorywithout understanding class dispatch - Ideal for complex, custom logic that doesn't fit a generic pattern
FBV weaknesses:
- Code duplication across similar views (every CRUD view re-implements the same authentication/pagination logic)
- HTTP method branching (
if request.method == 'POST') adds nesting and visual complexity - Sharing behaviour across views requires decorators or utility functions
Class-Based Views (CBVs)
A class-based view is a class that inherits from django.views.View (or a generic subclass). It maps HTTP methods to class methods:
from django.views import View
from django.http import HttpResponse
class HelloView(View):
def get(self, request):
return HttpResponse("Hello, GET")
def post(self, request):
return HttpResponse("Hello, POST")
Django's URL resolver expects a callable, so CBVs must be registered with .as_view():
# urls.py
path("hello/", HelloView.as_view(), name="hello")
CBV strengths:
- HTTP method dispatch is built in (
get(),post(),put(), etc.) - Mixins allow composable, reusable behaviour (authentication, pagination, permissions)
- Generic views (
ListView,CreateView, etc.) eliminate entire categories of boilerplate - Behaviour can be overridden at any level of the inheritance chain
- Central dispatch mechanism makes cross-cutting concerns clean
CBV weaknesses:
- Implicit behaviour — you need to know what the parent class does
- Debugging requires understanding method resolution order (MRO)
- Steeper learning curve, especially with deeply nested generics
- Over-engineering risk for simple views
Generic Class-Based Views
Django ships with a powerful set of pre-built CBVs for common patterns:
| Generic View | Purpose | Key Override Points |
|---|---|---|
TemplateView |
Render a template with context | get_context_data() |
ListView |
List objects with optional pagination |
get_queryset(), get_context_data()
|
DetailView |
Show one object |
get_object(), get_queryset()
|
CreateView |
Create an object via form |
form_valid(), get_form_class()
|
UpdateView |
Update an object via form |
form_valid(), get_object()
|
DeleteView |
Delete an object | get_success_url() |
FormView |
Handle any form (not tied to a model) |
form_valid(), form_invalid()
|
RedirectView |
Redirect to another URL | get_redirect_url() |
Architecture Design
The CBV Dispatch Mechanism
Understanding how CBVs work internally is the key to using them confidently. When an HTTP request arrives:
HTTP Request → urls.py → HelloView.as_view()
│
┌───────────────┘
▼
as_view() creates a closure:
def view(request, *args, **kwargs):
self = cls(**initkwargs) ← new instance per request
self.setup(request, *args, **kwargs)
return self.dispatch(request, *args, **kwargs)
│
▼
dispatch() maps method to handler:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
return handler(request, *args, **kwargs)
│
▼
self.get(request) or self.post(request)
A new class instance is created per request — this is critical. CBVs are not singletons. Instance attributes set during a request are not shared between requests.
The Mixin Resolution Chain
Mixins are classes that add behaviour to a CBV without being a full view themselves. Django's MRO (Method Resolution Order) determines which mixin's method wins when multiple mixins define the same method. The rule: leftmost wins.
class OrderDetailView(
LoginRequiredMixin, # ← checked 1st in dispatch()
PermissionRequiredMixin, # ← checked 2nd
OwnershipMixin, # ← checked 3rd (custom)
CacheMixin, # ← applied 4th
DetailView # ← base generic view
):
...
MRO resolution for dispatch():
LoginRequiredMixin.dispatch()
→ checks authentication → calls super()
↓
PermissionRequiredMixin.dispatch()
→ checks permissions → calls super()
↓
OwnershipMixin.dispatch()
→ checks object ownership → calls super()
↓
CacheMixin.dispatch()
→ checks cache → calls super() or returns cached response
↓
DetailView.dispatch()
→ routes to get() → fetches object → renders template
Each mixin calls super().dispatch() to pass control to the next mixin in the chain. If any mixin short-circuits (e.g., returns a 401 response), the chain stops there.
The Decision Framework
New view needed?
│
├── Webhook handler, one-off logic, complex custom flow?
│ → Use FBV
│
├── Simple CRUD on a model with standard templates?
│ → Use Generic CBV (ListView / CreateView / UpdateView / DeleteView)
│
├── API endpoint with standard REST patterns?
│ → Use DRF APIView or ViewSet (wraps CBV internally)
│
├── Custom logic but benefits from mixin composition?
│ → Use CBV (inherit from View, add mixins)
│
└── Shared behaviour across 5+ views (auth, rate limiting, logging)?
→ Define a Mixin, apply to all CBVs
Step-by-Step Implementation
Step 1: The Same View as FBV and CBV
Let's build a product detail view both ways to feel the difference concretely.
# ── FUNCTION-BASED VIEW ──
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from apps.catalog.models import Product
@login_required
@require_http_methods(["GET"])
def product_detail(request, slug: str):
product = get_object_or_404(
Product.objects.select_related("category").prefetch_related("tags"),
slug=slug,
status="active",
)
return render(request, "catalog/product_detail.html", {
"product": product,
"related": Product.objects.filter(category=product.category).exclude(pk=product.pk)[:4],
})
# ── CLASS-BASED VIEW ──
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import DetailView
from apps.catalog.models import Product
class ProductDetailView(LoginRequiredMixin, DetailView):
model = Product
template_name = "catalog/product_detail.html"
slug_field = "slug"
slug_url_kwarg = "slug"
def get_queryset(self):
return (
Product.objects
.select_related("category")
.prefetch_related("tags")
.filter(status="active")
)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["related"] = (
Product.objects
.filter(category=self.object.category)
.exclude(pk=self.object.pk)[:4]
)
return ctx
Both views do identical work. The FBV is slightly more readable for someone unfamiliar with DetailView. The CBV is more composable — you can add PermissionRequiredMixin, change pagination, or override object fetching by adding a single line.
Step 2: Registering Views in URLs
# apps/catalog/urls.py
from django.urls import path
from apps.catalog.views import product_detail, ProductDetailView
app_name = "catalog"
urlpatterns = [
# FBV — just pass the function
path("products/<slug:slug>/fbv/", product_detail, name="product-detail-fbv"),
# CBV — must call .as_view()
path("products/<slug:slug>/", ProductDetailView.as_view(), name="product-detail"),
]
Step 3: Applying Decorators to CBVs
Applying decorators to CBVs requires extra syntax because you're decorating a class, not a function:
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import DetailView
# Option 1: method_decorator on the class (applies to dispatch)
@method_decorator(cache_page(60 * 15), name="dispatch")
@method_decorator(vary_on_headers("Accept-Language"), name="dispatch")
class ProductDetailView(LoginRequiredMixin, DetailView):
...
# Option 2: method_decorator on a specific method
class ProductDetailView(LoginRequiredMixin, DetailView):
@method_decorator(cache_page(60 * 15))
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
# Option 3 (preferred): Use mixins instead of decorators on CBVs
# LoginRequiredMixin is cleaner than @method_decorator(login_required, name='dispatch')
Code Examples
Production FBV: Webhook Handler
FBVs are the right choice for complex, stateful handlers that don't fit any generic pattern — like webhook receivers:
# apps/payments/views.py
import hashlib
import hmac
import json
import logging
from django.conf import settings
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from apps.payments.tasks import process_stripe_event
logger = logging.getLogger(__name__)
@csrf_exempt
@require_POST
def stripe_webhook(request: HttpRequest) -> HttpResponse:
"""
Receives Stripe webhook events and dispatches to Celery for processing.
Why FBV? This view has:
- CSRF exemption (can't use mixin for this cleanly)
- Signature verification on raw bytes (before parsing)
- Complex branching on event type
- No "object" to fetch — pure event processing
All of these are cleaner in a flat function than a class.
"""
payload = request.body
sig_header = request.META.get("HTTP_STRIPE_SIGNATURE", "")
webhook_secret = settings.STRIPE_WEBHOOK_SECRET
# Verify Stripe signature BEFORE parsing JSON
# Use raw bytes — never decode before verification
expected_sig = hmac.new(
webhook_secret.encode("utf-8"),
payload,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected_sig, sig_header.split("=")[-1]):
logger.warning("Stripe webhook signature mismatch")
return HttpResponseBadRequest("Invalid signature")
try:
event = json.loads(payload)
except json.JSONDecodeError:
return HttpResponseBadRequest("Invalid JSON")
event_type = event.get("type", "")
logger.info("Stripe webhook received: %s", event_type)
# Dispatch to Celery — never process synchronously in a webhook handler
process_stripe_event.delay(event)
return HttpResponse(status=200)
Production CBV: Full CRUD with Mixin Chain
# apps/orders/views.py
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib import messages
from django.urls import reverse_lazy
from django.views.generic import (
ListView, DetailView, CreateView, UpdateView, DeleteView
)
from apps.orders.models import Order
from apps.orders.forms import OrderCreateForm, OrderUpdateForm
from apps.orders.mixins import OwnershipRequiredMixin, RateLimitMixin
class OrderListView(LoginRequiredMixin, ListView):
"""
Lists orders for the authenticated user.
Non-staff users only see their own orders.
Staff users see all orders.
"""
template_name = "orders/list.html"
context_object_name = "orders"
paginate_by = 20
def get_queryset(self):
qs = Order.objects.select_related("user").prefetch_related("items__product")
if not self.request.user.is_staff:
qs = qs.filter(user=self.request.user)
return qs.order_by("-created_at")
class OrderDetailView(LoginRequiredMixin, OwnershipRequiredMixin, DetailView):
"""
Shows order details. OwnershipRequiredMixin enforces that
the requesting user owns this order (unless staff).
"""
model = Order
template_name = "orders/detail.html"
context_object_name = "order"
def get_queryset(self):
return Order.objects.select_related("user", "shipping_address").prefetch_related("items__product")
class OrderCreateView(LoginRequiredMixin, RateLimitMixin, CreateView):
"""
Creates a new order. RateLimitMixin prevents order flooding.
"""
form_class = OrderCreateForm
template_name = "orders/create.html"
success_url = reverse_lazy("orders:list")
rate_limit = "10/hour" # ← read by RateLimitMixin
def form_valid(self, form):
form.instance.user = self.request.user
messages.success(self.request, "Order placed successfully.")
return super().form_valid(form)
class OrderUpdateView(LoginRequiredMixin, OwnershipRequiredMixin, UpdateView):
model = Order
form_class = OrderUpdateForm
template_name = "orders/update.html"
def get_success_url(self):
return reverse_lazy("orders:detail", kwargs={"pk": self.object.pk})
def form_valid(self, form):
messages.success(self.request, "Order updated.")
return super().form_valid(form)
class OrderDeleteView(LoginRequiredMixin, OwnershipRequiredMixin, DeleteView):
model = Order
template_name = "orders/confirm_delete.html"
success_url = reverse_lazy("orders:list")
def form_valid(self, form):
messages.success(self.request, f"Order #{self.object.pk} deleted.")
return super().form_valid(form)
Custom Mixins: Reusable Cross-Cutting Concerns
# apps/orders/mixins.py
import hashlib
import time
from django.core.cache import cache
from django.http import JsonResponse, HttpResponseForbidden
from django.contrib.auth.mixins import AccessMixin
class OwnershipRequiredMixin(AccessMixin):
"""
Enforces that request.user owns the object being accessed.
Staff users bypass this check (they can access anything).
Place AFTER LoginRequiredMixin in MRO — user must be authenticated first.
"""
ownership_field = "user" # ← field on the model that holds the owner FK
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
# Staff bypass
if request.user.is_staff:
return response
# Fetch the object if not already fetched
obj = getattr(self, "object", None)
if obj is None:
obj = self.get_object()
owner = getattr(obj, self.ownership_field, None)
owner_id = getattr(owner, "id", owner) # handle FK or plain id
if owner_id != request.user.id:
return HttpResponseForbidden("You do not have permission to access this resource.")
return response
class RateLimitMixin:
"""
Applies a simple sliding-window rate limit per authenticated user.
Reads the rate from self.rate_limit in the format "N/period"
where period is "minute", "hour", or "day".
Example: rate_limit = "10/hour"
"""
rate_limit = "100/hour"
def _parse_rate(self):
limit, period = self.rate_limit.split("/")
period_seconds = {"second": 1, "minute": 60, "hour": 3600, "day": 86400}
return int(limit), period_seconds.get(period, 3600)
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated:
limit, window = self._parse_rate()
bucket = int(time.time() // window)
key = f"rl:{request.user.id}:{self.__class__.__name__}:{bucket}"
count = cache.get(key, 0)
if count >= limit:
return JsonResponse(
{"error": "Rate limit exceeded. Please try again later."},
status=429,
)
cache.set(key, count + 1, timeout=window)
return super().dispatch(request, *args, **kwargs)
class AuditLogMixin:
"""
Logs every successful write operation (POST, PUT, PATCH, DELETE)
for audit trail purposes.
"""
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
if request.method in ("POST", "PUT", "PATCH", "DELETE") and response.status_code < 400:
import logging
logger = logging.getLogger("audit")
logger.info(
"audit_log",
extra={
"user_id": request.user.id if request.user.is_authenticated else None,
"method": request.method,
"path": request.path,
"view": self.__class__.__name__,
"status": response.status_code,
"ip": request.META.get("HTTP_X_FORWARDED_FOR", request.META.get("REMOTE_ADDR")),
},
)
return response
Django REST Framework: CBVs for API Views
In production APIs built on DRF, CBVs are near-universal. The APIView pattern gives you HTTP method dispatch, authentication, permission enforcement, and throttling all in one place:
# apps/api/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.throttling import UserRateThrottle
from apps.orders.serializers import OrderDetailSerializer, CreateOrderSerializer
from apps.orders.selectors import get_order_for_user, get_orders_for_user
from apps.orders.services import OrderService
class OrderListCreateAPIView(APIView):
"""
GET /api/orders/ → list user's orders
POST /api/orders/ → create a new order
Authentication, throttling, and pagination are class-level concerns.
Business logic lives in selectors and services — not the view.
"""
permission_classes = [IsAuthenticated]
throttle_classes = [UserRateThrottle]
def get(self, request):
orders = get_orders_for_user(user_id=request.user.id)
serializer = OrderDetailSerializer(orders, many=True)
return Response({
"count": orders.count(),
"results": serializer.data,
})
def post(self, request):
serializer = CreateOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
order = OrderService.create_order(
user=request.user,
**serializer.validated_data,
)
return Response(
{"id": order.id, "status": order.status},
status=status.HTTP_201_CREATED,
)
class OrderDetailAPIView(APIView):
"""
GET /api/orders/<pk>/ → retrieve order
PUT /api/orders/<pk>/ → full update
DELETE /api/orders/<pk>/ → cancel order
"""
permission_classes = [IsAuthenticated]
def get_object(self, pk: int):
return get_order_for_user(user=self.request.user, order_id=pk)
def get(self, request, pk: int):
order = self.get_object(pk)
return Response(OrderDetailSerializer(order).data)
def delete(self, request, pk: int):
order = self.get_object(pk)
OrderService.cancel_order(order)
return Response(status=status.HTTP_204_NO_CONTENT)
FBV with Decorator Stack: Maximum Explicit Control
# apps/reports/views.py
import csv
from django.contrib.auth.decorators import login_required, permission_required
from django.views.decorators.http import require_GET
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie
from django.http import StreamingHttpResponse
from apps.orders.models import Order
class EchoWriter:
def write(self, value):
return value
@login_required
@permission_required("orders.view_order", raise_exception=True)
@require_GET
@cache_page(60 * 5) # 5-minute page cache
@vary_on_cookie # Separate cache per session
def export_orders_csv(request):
"""
Streams a large orders CSV to the response.
FBV is the right choice here — this is a one-off export view
with specific HTTP method requirements, multiple decorators,
and custom streaming behaviour that CBVs don't simplify.
"""
writer = csv.writer(EchoWriter())
def generate():
yield writer.writerow(["ID", "User", "Status", "Total", "Date"])
qs = (
Order.objects
.filter(user=request.user)
.select_related("user")
.only("id", "user__email", "status", "total", "created_at")
.iterator(chunk_size=1000)
)
for order in qs:
yield writer.writerow([
order.id, order.user.email, order.status,
str(order.total), order.created_at.date(),
])
response = StreamingHttpResponse(generate(), content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename="my_orders.csv"'
return response
Performance Optimization
1. Override get_queryset() in Generic CBVs — Never Use queryset = Directly
# BAD — queryset is evaluated once at class definition time
class ProductListView(ListView):
queryset = Product.objects.filter(status="active") # ← evaluated at import!
# GOOD — get_queryset() is called per request, allowing dynamic filtering
class ProductListView(ListView):
def get_queryset(self):
qs = Product.objects.filter(status="active").select_related("category")
if q := self.request.GET.get("q"):
qs = qs.filter(name__icontains=q)
return qs
2. Cache at the View Layer with cache_page
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
# FBV
@cache_page(60 * 15)
@vary_on_headers("Accept-Language")
def product_list(request):
...
# CBV — apply to dispatch or specific methods
@method_decorator(cache_page(60 * 15), name="dispatch")
class ProductListView(ListView):
...
3. Use only() in View QuerySets for List Views
List views often only display a subset of model fields. Don't load what you don't need:
class OrderListView(LoginRequiredMixin, ListView):
paginate_by = 25
def get_queryset(self):
return (
Order.objects
.filter(user=self.request.user)
# Load only the columns actually used in the template
.only("id", "status", "total", "created_at")
.order_by("-created_at")
)
4. Optimise Pagination for Large Datasets
Django's built-in paginate_by uses COUNT(*) to determine total pages, which is expensive on large tables. For high-traffic list views, use cursor-based pagination instead:
class OrderListView(LoginRequiredMixin, ListView):
paginate_by = 20
def get_queryset(self):
qs = Order.objects.filter(user=self.request.user).order_by("-created_at")
# Cursor-based: use the last seen ID to avoid COUNT(*)
after_id = self.request.GET.get("after")
if after_id:
qs = qs.filter(id__lt=after_id)
return qs[:self.paginate_by + 1] # fetch one extra to detect next page
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
orders = list(ctx["orders"])
ctx["has_next"] = len(orders) > self.paginate_by
ctx["orders"] = orders[:self.paginate_by]
if orders:
ctx["next_cursor"] = orders[-1].id
return ctx
5. Use get_object() Caching in CBVs
get_object() in CBVs queries the database every time it's called. Cache it:
from django.utils.functional import cached_property
class OrderDetailView(LoginRequiredMixin, OwnershipRequiredMixin, DetailView):
model = Order
@cached_property
def object(self):
"""
Override object as a cached_property to avoid multiple DB hits.
get_object() is called in OwnershipRequiredMixin.dispatch()
AND in DetailView.get() — without caching, that's two queries.
"""
return self.get_object()
Security Best Practices
1. Always Put LoginRequiredMixin First
# CORRECT — MRO ensures authentication check happens before anything else
class OrderDetailView(LoginRequiredMixin, OwnershipRequiredMixin, DetailView):
...
# WRONG — OwnershipRequiredMixin might call get_object() before auth check
class OrderDetailView(OwnershipRequiredMixin, LoginRequiredMixin, DetailView):
...
Best practices prioritise mixins over @method_decorator(login_required) for CBVs due to cleaner inheritance and MRO compliance.
2. Object-Level Authorisation — Never Trust URL Parameters Alone
# DANGEROUS — any authenticated user can access any order by ID
class OrderDetailView(LoginRequiredMixin, DetailView):
model = Order
# ← No ownership check! IDOR vulnerability.
# SAFE — always scope get_queryset to the authenticated user
class OrderDetailView(LoginRequiredMixin, DetailView):
def get_queryset(self):
# Scoping here means get_object() raises 404 for orders not owned by this user
return Order.objects.filter(user=self.request.user)
3. CSRF Protection for Custom API Views
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views import View
# Webhook handlers that receive external POST requests must be CSRF-exempt
# But document WHY — missing CSRF is a common security review finding
@method_decorator(csrf_exempt, name="dispatch")
class StripeWebhookView(View):
"""
CSRF exempt: requests come from Stripe servers, not browsers.
Security is handled by Stripe's HMAC signature verification instead.
"""
def post(self, request):
...
4. Set raise_exception=True for API Views
For API views, redirecting unauthenticated users to a login page makes no sense. Raise a 403 instead:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import DetailView
class APIOrderDetailView(LoginRequiredMixin, DetailView):
# Return 403 Forbidden instead of redirecting to login page
# Essential for AJAX/API endpoints consumed by frontend apps
raise_exception = True
def handle_no_permission(self):
from django.http import JsonResponse
return JsonResponse({"error": "Authentication required."}, status=401)
5. Rate Limiting as a Mixin (Not Just Middleware)
Middleware applies globally. View-level rate limiting lets you set different limits for different endpoints:
class OrderCreateView(LoginRequiredMixin, RateLimitMixin, CreateView):
rate_limit = "5/hour" # ← strict: orders are high-value operations
class ProductSearchView(LoginRequiredMixin, RateLimitMixin, ListView):
rate_limit = "300/minute" # ← lenient: search is read-only
Common Developer Mistakes
❌ Mistake 1: Putting Business Logic in the View
# BAD — the view does everything: validates, queries, transforms, emails
class OrderCreateView(LoginRequiredMixin, CreateView):
def form_valid(self, form):
product = Product.objects.get(id=form.cleaned_data["product_id"])
if product.stock < form.cleaned_data["quantity"]:
form.add_error(None, "Insufficient stock")
return self.form_invalid(form)
order = form.save(commit=False)
order.user = self.request.user
order.total = product.price * form.cleaned_data["quantity"]
order.save()
product.stock -= form.cleaned_data["quantity"]
product.save()
send_mail("Order confirmed", ..., [self.request.user.email])
return super().form_valid(form)
# GOOD — view delegates to a service; logic is testable without HTTP
class OrderCreateView(LoginRequiredMixin, CreateView):
def form_valid(self, form):
order = OrderService.create_order(
user=self.request.user,
**form.cleaned_data,
)
return HttpResponseRedirect(reverse("orders:detail", kwargs={"pk": order.pk}))
❌ Mistake 2: Using queryset = as a Class Attribute in CBVs
# BAD — evaluated ONCE at class definition time, not per request
class ProductListView(ListView):
queryset = Product.objects.filter(status="active")
# Django caches this; changes to the DB not reflected without restart
# GOOD — evaluated per request
class ProductListView(ListView):
def get_queryset(self):
return Product.objects.filter(status="active")
❌ Mistake 3: Forgetting .as_view() in URLs
# BAD — Django will raise a TypeError when the URL is hit
path("orders/", OrderListView, name="order-list") # ← missing .as_view()
# GOOD
path("orders/", OrderListView.as_view(), name="order-list")
❌ Mistake 4: Wrong Mixin Order
# BAD — RateLimitMixin tries to access self.object before LoginRequiredMixin checks auth
class OrderDetailView(RateLimitMixin, LoginRequiredMixin, DetailView):
...
# GOOD — authentication first, then rate limiting, then ownership, then logic
class OrderDetailView(LoginRequiredMixin, RateLimitMixin, OwnershipRequiredMixin, DetailView):
...
❌ Mistake 5: Using FBVs for Everything Just to Avoid Learning CBVs
# BAD — 5 near-identical CRUD FBVs with copy-pasted logic
def product_list(request): ...
def product_detail(request, pk): ...
def product_create(request): ...
def product_update(request, pk): ...
def product_delete(request, pk): ...
# GOOD — 5 CBVs that each override only what differs
class ProductListView(LoginRequiredMixin, ListView): ...
class ProductDetailView(LoginRequiredMixin, DetailView): ...
class ProductCreateView(LoginRequiredMixin, CreateView): ...
class ProductUpdateView(LoginRequiredMixin, UpdateView): ...
class ProductDeleteView(LoginRequiredMixin, DeleteView): ...
Real Production Use Cases
Use Case 1: SaaS Dashboard — Tenant-Scoped CBV Mixin
# common/mixins.py
class TenantScopedMixin:
"""
Scopes all querysets to the current tenant.
Attach to every CBV in a multi-tenant SaaS application.
The tenant is resolved by TenantResolutionMiddleware and attached to request.tenant.
"""
def get_queryset(self):
qs = super().get_queryset()
if hasattr(self.request, "tenant"):
qs = qs.filter(tenant=self.request.tenant)
return qs
# Usage: one mixin, consistent scoping everywhere
class ProjectListView(LoginRequiredMixin, TenantScopedMixin, ListView):
model = Project
paginate_by = 20
class ProjectDetailView(LoginRequiredMixin, TenantScopedMixin, DetailView):
model = Project
Use Case 2: E-Commerce — Layered Mixin Chain for Checkout
class CheckoutView(
LoginRequiredMixin, # Must be authenticated
RateLimitMixin, # Max 5 checkouts per hour
AuditLogMixin, # Every checkout logged for compliance
TemplateView,
):
template_name = "checkout/index.html"
rate_limit = "5/hour"
raise_exception = True # API consumer — return 403, not redirect
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["cart"] = CartService.get_or_create(self.request.user)
ctx["payment_intent"] = PaymentService.create_intent(ctx["cart"])
return ctx
def post(self, request, *args, **kwargs):
serializer = CheckoutSerializer(data=request.POST)
serializer.is_valid(raise_exception=True)
order = OrderService.create_from_cart(
user=request.user,
**serializer.validated_data,
)
return redirect(reverse("orders:confirmation", kwargs={"pk": order.pk}))
Use Case 3: REST API — FBVs for Complex Custom Endpoints
Not every API endpoint maps cleanly to a resource. For operations — "cancel order", "resend confirmation", "bulk-archive products" — FBVs with DRF decorators are often cleaner:
from rest_framework.decorators import api_view, permission_classes, throttle_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.throttling import UserRateThrottle
@api_view(["POST"])
@permission_classes([IsAuthenticated])
@throttle_classes([UserRateThrottle])
def cancel_order(request, pk: int):
"""
POST /api/orders/<pk>/cancel/
This is an operation, not a resource. It doesn't fit
ListView/DetailView/CreateView. FBV with DRF decorators
is the cleanest approach.
"""
from apps.orders.selectors import get_order_for_user
from apps.orders.services import OrderService
order = get_order_for_user(user=request.user, order_id=pk)
OrderService.cancel_order(order)
return Response({"status": "cancelled"})
Conclusion
Function-based views and class-based views are not competing philosophies — they are complementary tools with different strengths.
Use FBVs when:
- The view logic is unique and doesn't generalise
- You need maximum clarity for complex, custom flows
- You're handling webhooks, streaming responses, or operations (not resources)
- You want to stack decorators without worrying about MRO
Use CBVs when:
- You're implementing standard CRUD patterns on a model
- You need to share behaviour across multiple views via mixins
- You want authentication, rate limiting, and audit logging applied consistently at the class level
- You're building a REST API where HTTP method dispatch is structural
The most important principle, regardless of which you choose: keep views thin. A view's job is HTTP. It should accept a request, call a service or selector, and return a response. Business logic lives in services. Database logic lives in selectors and QuerySets. Cross-cutting concerns live in middleware or mixins.
A thin view is readable regardless of whether it's a function or a class. A fat view is hard to maintain regardless of which style you chose.
Build views that do one thing. Compose them with mixins when you need to. Choose the form that makes the intent clearest — and change your mind when the requirements change.
Further Reading
- Django Class-Based Views Documentation
- Classy Class-Based Views — ccbv.co.uk ← Essential reference for CBV inheritance chains
- Django Views — The Right Way (Luke Plant) ← Strong FBV advocacy
- Django Best Practices: FBVs vs CBVs — LearnDjango
- TestDriven.io: Class-Based vs Function-Based Views
- Django REST Framework: APIView
- Django Auth Mixins
Written by a Python backend engineer building production Django systems. Topics: Django views, FBV, CBV, generic views, mixins, LoginRequiredMixin, PermissionRequiredMixin, rate limiting, CSRF, DRF APIView, production patterns.
Top comments (0)