Django Views Explained – Functions vs Classes and When to Use Each

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.

Author

  • Mohammad Golam Dostogir, Software Engineer specializing in Python, Django, and AI solutions. Active contributor to open-source projects and tech communities, with experience delivering applications for global companies.
    GitHub

    View all posts
Index