Views are where your Django application comes to life. They’re the bridge between your beautiful models and the templates that users actually see. But here’s the thing that confused me for months when I started – Django gives you two completely different ways to write views.
Function-based views (FBVs) and class-based views (CBVs) each have their place. After writing hundreds of views in production applications, I finally understand when to use each approach. Let me save you the confusion I went through.
Function-Based Views – Simple and Straightforward
Function-based views are exactly what they sound like – Python functions that take a request and return a response. They’re perfect when your logic is straightforward and you don’t need fancy inheritance.
Let’s start with our bookstore’s book listing page:
# inventory/views.py
from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse
from django.core.paginator import Paginator
from django.db.models import Q
from .models import Book, Category, Author
def book_list(request):
"""Display a paginated list of books with filtering options"""
books = Book.objects.available().select_related('category').prefetch_related('authors')
# Handle search
search_query = request.GET.get('search')
if search_query:
books = books.filter(
Q(title__icontains=search_query) |
Q(authors__first_name__icontains=search_query) |
Q(authors__last_name__icontains=search_query)
).distinct()
# Handle category filtering
category_slug = request.GET.get('category')
if category_slug:
books = books.filter(category__slug=category_slug)
# Handle sorting
sort_by = request.GET.get('sort', 'newest')
if sort_by == 'price_low':
books = books.order_by('price')
elif sort_by == 'price_high':
books = books.order_by('-price')
elif sort_by == 'title':
books = books.order_by('title')
else: # newest
books = books.order_by('-publication_date')
# Pagination
paginator = Paginator(books, 12) # 12 books per page
page_number = request.GET.get('page')
page_books = paginator.get_page(page_number)
# Get all categories for the filter sidebar
categories = Category.objects.filter(is_active=True).order_by('name')
context = {
'books': page_books,
'categories': categories,
'current_category': category_slug,
'search_query': search_query,
'current_sort': sort_by,
}
return render(request, 'inventory/book_list.html', context)
def book_detail(request, pk):
"""Display detailed information about a specific book"""
book = get_object_or_404(Book, pk=pk, is_available=True)
# Get related books from the same category
related_books = Book.objects.filter(
category=book.category,
is_available=True
).exclude(pk=book.pk).select_related('category')[:4]
# Get approved reviews
reviews = book.reviews.filter(is_approved=True).order_by('-created_at')[:10]
context = {
'book': book,
'related_books': related_books,
'reviews': reviews,
'average_rating': book.get_average_rating(),
'rating_distribution': book.get_rating_distribution(),
}
return render(request, 'inventory/book_detail.html', context)
def category_detail(request, slug):
"""Show all books in a specific category"""
category = get_object_or_404(Category, slug=slug, is_active=True)
books = Book.objects.filter(category=category, is_available=True)
# Simple pagination for category pages
paginator = Paginator(books, 16)
page_number = request.GET.get('page')
page_books = paginator.get_page(page_number)
context = {
'category': category,
'books': page_books,
}
return render(request, 'inventory/category_detail.html', context)
AJAX Views for Dynamic Content
Function-based views are perfect for AJAX endpoints that return JSON data:
def book_search_ajax(request):
"""AJAX endpoint for live book search"""
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
query = request.GET.get('q', '')
if len(query) > 2: # Only search if query is longer than 2 characters
books = Book.objects.filter(
Q(title__icontains=query) | Q(authors__last_name__icontains=query),
is_available=True
).select_related('category')[:10]
results = []
for book in books:
results.append({
'id': book.pk,
'title': book.title,
'authors': ', '.join([author.full_name for author in book.authors.all()]),
'price': str(book.price),
'category': book.category.name,
'url': book.get_absolute_url(),
})
return JsonResponse({'results': results})
return JsonResponse({'results': []})
Class-Based Views – Power Through Inheritance
Class-based views shine when you need to reuse code or customize existing behavior. Django’s generic views handle common patterns like listing and detail pages.
ListView for Displaying Multiple Objects
from django.views.generic import ListView, DetailView
from django.db.models import Q
class BookListView(ListView):
model = Book
template_name = 'inventory/book_list.html'
context_object_name = 'books'
paginate_by = 12
def get_queryset(self):
"""Customize the queryset with filtering and searching"""
queryset = Book.objects.available().select_related('category').prefetch_related('authors')
# Apply search filter
search_query = self.request.GET.get('search')
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query) |
Q(authors__first_name__icontains=search_query) |
Q(authors__last_name__icontains=search_query)
).distinct()
# Apply category filter
category_slug = self.request.GET.get('category')
if category_slug:
queryset = queryset.filter(category__slug=category_slug)
# Apply sorting
sort_by = self.request.GET.get('sort', 'newest')
sort_options = {
'price_low': 'price',
'price_high': '-price',
'title': 'title',
'newest': '-publication_date'
}
queryset = queryset.order_by(sort_options.get(sort_by, '-publication_date'))
return queryset
def get_context_data(self, **kwargs):
"""Add extra context for the template"""
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.filter(is_active=True).order_by('name')
context['current_category'] = self.request.GET.get('category')
context['search_query'] = self.request.GET.get('search')
context['current_sort'] = self.request.GET.get('sort', 'newest')
return context
class BookDetailView(DetailView):
model = Book
template_name = 'inventory/book_detail.html'
context_object_name = 'book'
def get_queryset(self):
"""Only show available books"""
return Book.objects.filter(is_available=True)
def get_context_data(self, **kwargs):
"""Add related books and reviews to the context"""
context = super().get_context_data(**kwargs)
book = self.object
context['related_books'] = Book.objects.filter(
category=book.category,
is_available=True
).exclude(pk=book.pk).select_related('category')[:4]
context['reviews'] = book.reviews.filter(is_approved=True).order_by('-created_at')[:10]
context['average_rating'] = book.get_average_rating()
context['rating_distribution'] = book.get_rating_distribution()
return context
Custom Mixins for Reusable Behavior
This is where class-based views really shine. You can create mixins that add functionality to multiple views:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView, UpdateView
from django.urls import reverse_lazy
class BookFormMixin:
"""Common functionality for book forms"""
model = Book
fields = ['title', 'subtitle', 'authors', 'category', 'isbn_13', 'publisher',
'publication_date', 'pages', 'price', 'cost_price', 'stock_quantity',
'description', 'featured']
def form_valid(self, form):
"""Add custom logic when form is valid"""
if not self.request.user.has_perm('inventory.add_book'):
messages.error(self.request, "You don't have permission to add books.")
return self.form_invalid(form)
return super().form_valid(form)
class BookCreateView(LoginRequiredMixin, BookFormMixin, CreateView):
template_name = 'inventory/book_form.html'
success_url = reverse_lazy('inventory:book-list')
def form_valid(self, form):
"""Set the book as featured if user is staff"""
response = super().form_valid(form)
if self.request.user.is_staff:
messages.success(self.request, f"Book '{self.object.title}' created successfully!")
return response
class BookUpdateView(LoginRequiredMixin, BookFormMixin, UpdateView):
template_name = 'inventory/book_form.html'
def get_success_url(self):
return reverse_lazy('inventory:book-detail', kwargs={'pk': self.object.pk})
URL Configuration That Makes Sense
Now let’s wire up our views with clean, RESTful URLs:
# inventory/urls.py
from django.urls import path
from . import views
app_name = 'inventory'
urlpatterns = [
# Function-based views
path('', views.book_list, name='book-list'),
path('book//', views.book_detail, name='book-detail'),
path('category//', views.category_detail, name='category-detail'),
path('ajax/search/', views.book_search_ajax, name='book-search-ajax'),
# Class-based views
path('books/', views.BookListView.as_view(), name='book-list-cbv'),
path('books//', views.BookDetailView.as_view(), name='book-detail-cbv'),
path('books/add/', views.BookCreateView.as_view(), name='book-create'),
path('books//edit/', views.BookUpdateView.as_view(), name='book-update'),
]
When to Use Functions vs Classes
After building dozens of Django applications, here’s my rule of thumb:
Use Function-Based Views When:
- Your logic is simple and straightforward
- You’re building AJAX endpoints that return JSON
- You need fine-grained control over every step
- The view does something unique that doesn’t fit Django’s patterns
- You’re new to Django and want to understand what’s happening
Use Class-Based Views When:
- You’re doing standard CRUD operations
- You want to reuse code across multiple views
- You need to customize Django’s generic views
- You’re building a large application with many similar views
- You want to use Django’s built-in mixins for authentication, pagination, etc.
Performance Tips for Your Views
Both types of views can be optimized. Here are the techniques I use in production:
# Use select_related and prefetch_related to avoid N+1 queries
def optimized_book_list(request):
books = Book.objects.select_related('category').prefetch_related('authors')
# This loads all related data in just 2 queries instead of N+1
return render(request, 'inventory/book_list.html', {'books': books})
# Cache expensive operations
from django.core.cache import cache
def cached_bestsellers(request):
bestsellers = cache.get('bestsellers')
if bestsellers is None:
bestsellers = Book.objects.available()[:10]
cache.set('bestsellers', bestsellers, 3600) # Cache for 1 hour
return render(request, 'inventory/bestsellers.html', {'books': bestsellers})
Error Handling in Views
Real applications need robust error handling:
from django.contrib import messages
from django.http import Http404
def safe_book_detail(request, pk):
"""Book detail view with proper error handling"""
try:
book = Book.objects.select_related('category').get(pk=pk, is_available=True)
except Book.DoesNotExist:
messages.error(request, "Sorry, that book is not available.")
return redirect('inventory:book-list')
context = {'book': book}
return render(request, 'inventory/book_detail.html', context)
Testing Your Views
Good views deserve good tests:
# inventory/tests.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from .models import Book, Category, Author
class BookViewTests(TestCase):
def setUp(self):
self.client = Client()
self.category = Category.objects.create(name='Fiction', slug='fiction')
self.author = Author.objects.create(first_name='Test', last_name='Author')
self.book = Book.objects.create(
title='Test Book',
isbn_13='9781234567890',
category=self.category,
price=19.99,
cost_price=12.00,
stock_quantity=10,
publication_date='2022-01-01',
pages=200,
publisher='Test Publisher',
description='A test book'
)
self.book.authors.add(self.author)
def test_book_list_view(self):
response = self.client.get(reverse('inventory:book-list'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Test Book')
def test_book_detail_view(self):
response = self.client.get(reverse('inventory:book-detail', kwargs={'pk': self.book.pk}))
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.book.title)
def test_book_search(self):
response = self.client.get(reverse('inventory:book-list'), {'search': 'Test'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Test Book')
What’s Coming Next
We’ve built solid views that handle all the common patterns you’ll need in a real application. The combination of function-based and class-based views gives you the flexibility to handle any scenario.
In my next article, we’ll dive into Django templates and create beautiful, maintainable HTML that brings our views to life. I’ll show you template inheritance, custom tags, and the patterns I use to keep templates organized in large projects.
The views we built today handle pagination, filtering, searching, and error cases – everything you need for a professional application. Take time to understand both patterns, and you’ll be able to choose the right tool for each situation.