Writing a separate function view for every list page and detail page means repeating the same pattern over and over: fetch a queryset, sort it, paginate it, or call get_object_or_404 and render a template. ListView and DetailView are Django’s built-in class-based views that do exactly those jobs — you only declare the model, the queryset, and the template, and the framework handles the rest. This post replaces two hand-written function views in a blog app with these two classes.

The Problem

A typical blog app has two pages that repeat the same shape: a list of posts and the detail page of a single post. Written as function views, both get verbose: fetch a queryset, filter published posts, sort them, build a Paginator by hand and pick the current page, or call get_object_or_404 by slug. Write three or four pages like this and you are copying the same pattern again and again — and every time you change the pagination or filtering logic, you have to change it in several places.

# views.py — hàm view thủ công, lặp lại cùng một mẫu
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404, render
from .models import Post
def post_list(request):
posts = Post.objects.filter(published=True).order_by("-created_at")
paginator = Paginator(posts, 10)
page = paginator.get_page(request.GET.get("page"))
return render(request, "blog/post_list.html", {"page_obj": page})
def post_detail(request, slug):
post = get_object_or_404(Post, slug=slug, published=True)
return render(request, "blog/post_detail.html", {"post": post})

The Solution

ListView and DetailView package exactly that pattern. ListView fetches the queryset itself, paginates it, and passes page_obj, paginator, and is_paginated into the template. DetailView looks up the object by slug or primary key on its own, and returns a 404 by itself when nothing matches. You only declare what differs: which model, how to filter, which template, and how many items per page.

# views.py — class-based view, khai báo thay vì viết lại logic
from django.views.generic import DetailView, ListView
from .models import Post
class PostListView(ListView):
model = Post
queryset = Post.objects.filter(published=True).order_by("-created_at")
template_name = "blog/post_list.html"
context_object_name = "posts"
paginate_by = 10
class PostDetailView(DetailView):
model = Post
queryset = Post.objects.filter(published=True)
template_name = "blog/post_detail.html"
context_object_name = "post"
# urls.py — nhớ gọi .as_view() khi khai báo route
from django.urls import path
from .views import PostDetailView, PostListView
urlpatterns = [
path("", PostListView.as_view(), name="post-list"),
path("<slug:slug>/", PostDetailView.as_view(), name="post-detail"),
]

In the list template, you loop over posts and use page_obj.has_next to draw the next-page button, because paginate_by already turns pagination on. In the detail template, the post variable is exactly the object DetailView found. If you need to filter by a URL parameter — for example, listing only the posts of one category — override get_queryset() and read self.kwargs instead of declaring a fixed queryset. When a page’s logic gets more complex than a generic class can handle, go back to a function view instead of forcing everything into ListView.

Apply It to Your Project

Open your Django app and look for function views that do just three things: fetch a list, sort it, and paginate it — or fetch one object by slug and render it. Replace each one with a ListView or DetailView, re-run the app’s tests to make sure the template receives the right context variables, and only then move on to the next page. The remaining pages — forms, POST handling, logic that pulls from several data sources — can stay as function views.

Related Posts

Frequently Asked Questions

When should I use a function view instead of ListView or DetailView?

When the page needs complex logic: handling a POST form, combining several data sources, or running custom calculations before rendering. Generic classes only stay tidy when the page follows the pattern of fetching a list or fetching one object and displaying it.

Which template does ListView use if I don’t declare template_name?

Django follows the convention <app>/<model>_list.html for ListView and <app>/<model>_detail.html for DetailView, and the default context variable is object_list and object. Declaring template_name and context_object_name explicitly saves the next person reading the code from guessing.

How do I filter the list by a parameter from the URL?

Override get_queryset() and use self.kwargs to read the parameter, for example filtering Post by category__slug=self.kwargs["slug"]. ListView’s pagination and ordering stay exactly as they were.

Which variables does pagination put in the template?

When you set paginate_by, ListView already puts page_obj, paginator, and is_paginated into the context. Use page_obj.has_next and page_obj.next_page_number to draw the “Next page” button — there is no need to build a Paginator yourself anymore.

What if the URL uses a primary key instead of a slug?

By default DetailView reads the pk parameter from the URL and looks the object up by primary key. If the URL uses a slug, it reads the slug parameter and looks up the slug field — change the field with slug_field when needed.

💬 Have you tried this in your own project? If you hit an error or know a shorter way to do it, leave a comment below — I read them all and will add the best ideas to the post.

References