๐ฆ Views and URL Patterns
If models are Django's memory, views and URLs are its nervous system. The URLconf decides which code runs for a given address; the view decides what that code does. Master this pair and you can turn any request into any response โ a page, a redirect, or JSON.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Route requests with URLconf, path converters, and named namespaces
- Write function-based views that return pages, redirects, and JSON
- Handle form submissions safely with the Post/Redirect/Get pattern
- Use generic class-based views (List, Detail, Create, Update, Delete) to slash boilerplate
- Reuse logic with mixins and avoid hard-coded links with reverse URL resolution
Estimated Time: 40โ50 minutes โข Difficulty: Intermediate
Hands-on: Build a list-and-detail pair two ways โ as a function and as a class-based view.
In This Lesson
How Views and URLs Fit Together
Together, URL patterns and views are the "controller" of Django's MVT architecture. The division of labor is clean:
- URL patterns match an incoming path to a view.
- Views take the request, do the work, and return a response.
๐ฎ The postal-service analogy: The URLconf is the sorting facility that reads the address and routes each letter; the view is the clerk at the destination who opens it and acts. Path parameters are the apartment number that pinpoints the recipient, and HTTP methods (GET, POST) are the class of mail โ a letter versus a parcel.
URL Configuration
URLs live in urls.py files, usually at two levels. The project URLconf is the front door; it hands path prefixes off to each app's URLconf with include().
Project-level URLs
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls")),
path("accounts/", include("accounts.urls")),
path("", include("pages.urls")),
]
# Serve uploaded media during development only
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
App-level URLs
Each app defines its own routes and โ importantly โ sets an app_name so URL names never collide between apps:
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog" # namespace for URL names
urlpatterns = [
path("", views.post_list, name="post_list"),
path("<int:year>/<int:month>/<int:day>/<slug:slug>/",
views.post_detail, name="post_detail"),
path("category/<slug:category_slug>/",
views.post_list_by_category, name="post_list_by_category"),
path("search/", views.post_search, name="post_search"),
]
๐ Why namespaces matter
With app_name = "blog", you reference a route as "blog:post_detail". Two apps can each have a post_detail without ambiguity โ a small habit that prevents painful refactors later.
Path Converters
The angle-bracket syntax in a path โ <int:year> โ is a path converter. It captures a URL segment, validates its shape, converts it to a Python type, and passes it to the view as a keyword argument.
| Converter | Matches | Example |
|---|---|---|
str | Any non-empty text without / | <str:username> |
int | Zero or a positive integer | <int:year> |
slug | Letters, numbers, hyphens, underscores | <slug:post_slug> |
uuid | A UUID-formatted string | <uuid:id> |
path | Any text, including / | <path:file_path> |
from django.urls import path
from . import views
urlpatterns = [
path("about/", views.about, name="about"),
path("product/<slug:slug>/", views.product_detail, name="product_detail"),
path("archive/<int:year>/<int:month>/", views.archive, name="archive"),
path("order/<uuid:order_id>/", views.order_detail, name="order_detail"),
]
When a plain converter isn't precise enough, re_path lets you use a regular expression โ powerful, but harder to read, so reach for it only when you must:
from django.urls import re_path
urlpatterns = [
# A 4-digit year and a valid month 01-12
re_path(r"^archive/(?P<year>[0-9]{4})/(?P<month>0[1-9]|1[0-2])/$",
views.archive, name="archive"),
]
Function-Based Views
The simplest view is a function: it takes a request (plus any captured URL parameters) and returns a response.
from django.shortcuts import render, get_object_or_404
from .models import Post
def post_list(request):
"""Show all published posts."""
posts = Post.published.all()
return render(request, "blog/post_list.html", {"posts": posts})
def post_detail(request, year, month, day, slug):
"""Show a single post, or 404."""
post = get_object_or_404(
Post, slug=slug, status=Post.Status.PUBLISHED,
published_at__year=year, published_at__month=month, published_at__day=day,
)
comments = post.comments.filter(approved=True)
return render(request, "blog/post_detail.html",
{"post": post, "comments": comments})
๐ก Reach for get_object_or_404
Calling .get() raises DoesNotExist and produces an ugly 500 error when a row is missing. get_object_or_404() returns a clean 404 instead โ the right behavior for "this page doesn't exist."
The request object
request.method # "GET", "POST", ...
request.GET # query-string parameters
request.POST # submitted form data
request.FILES # uploaded files
request.user # the logged-in User (or AnonymousUser)
request.path # the URL path
Response types
from django.shortcuts import render, redirect
from django.http import HttpResponse, JsonResponse
def html_view(request):
return render(request, "template.html", {"key": "value"})
def redirect_view(request):
return redirect("blog:post_list") # by URL name
def api_view(request):
return JsonResponse({"items": [1, 2, 3]})
def teapot(request):
return HttpResponse("Not found", status=404)
Form Handling & PRG
A view that accepts input branches on the request method: show an empty form on GET, process the submission on POST.
from django.shortcuts import render, redirect, get_object_or_404
from .forms import CommentForm
from .models import Post
def add_comment(request, post_id):
post = get_object_or_404(Post, id=post_id, status=Post.Status.PUBLISHED)
if request.method == "POST":
form = CommentForm(request.POST)
if form.is_valid():
comment = form.save(commit=False) # build but don't save yet
comment.post = post
if request.user.is_authenticated:
comment.author = request.user
comment.save()
return redirect(post.get_absolute_url()) # PRG: redirect after POST
else:
form = CommentForm()
return render(request, "blog/add_comment.html", {"form": form, "post": post})
โ The Post/Redirect/Get (PRG) pattern
Always redirect after a successful POST. Then a browser refresh re-requests the destination page (a GET) instead of resubmitting the form โ no duplicate comments, no "confirm form resubmission" warning.
- User POSTs the form.
- Server saves and redirects.
- Browser GETs the success page.
โ ๏ธ Don't disable CSRF protection
Every HTML form that POSTs needs a {% csrf_token %} tag, and Django's CsrfViewMiddleware must stay enabled. It's your defense against cross-site request forgery โ turning it off to "make the form work" opens a serious hole.
Class-Based Views
Class-based views (CBVs) package view logic into classes. Their real payoff is Django's generic CBVs, which implement the common patterns โ listing, showing, creating, editing, deleting โ so you write only what's different.
Compare the function version of a list to the generic ListView:
from django.views.generic import ListView, DetailView
from .models import Post
class PostListView(ListView):
model = Post
template_name = "blog/post_list.html"
context_object_name = "posts"
paginate_by = 10 # free pagination!
def get_queryset(self):
return Post.published.all()
class PostDetailView(DetailView):
model = Post
template_name = "blog/post_detail.html"
context_object_name = "post"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["comments"] = self.object.comments.filter(approved=True)
return context
Wire a CBV into the URLconf with .as_view():
path("", views.PostListView.as_view(), name="post_list"),
path("<int:pk>/", views.PostDetailView.as_view(), name="post_detail"),
The editing trio โ CreateView, UpdateView, DeleteView โ builds full CRUD from a model and a form:
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
form_class = PostForm
template_name = "blog/post_form.html"
def form_valid(self, form):
form.instance.author = self.request.user # stamp the author
return super().form_valid(form)
class PostDeleteView(LoginRequiredMixin, DeleteView):
model = Post
success_url = reverse_lazy("blog:post_list")
The generic CBVs form an inheritance hierarchy you can lean on:
Mixins: reusable slices of behavior
Mixins add capabilities to a view through multiple inheritance. The most common are access checks:
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import UpdateView
from .models import Post
from .forms import PostForm
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
form_class = PostForm
template_name = "blog/post_form.html"
def test_func(self):
# Only the author may edit their own post
return self.request.user == self.get_object().author
โ ๏ธ Mixin order counts
Access-control mixins must appear before the generic view in the class's base list (left to right), so their checks run first. class V(LoginRequiredMixin, UpdateView) โ never the reverse.
FBV vs. CBV โ when to use which
| Use a function-based view whenโฆ | Use a class-based view whenโฆ |
|---|---|
| The logic is simple or one-off | The view fits a standard pattern (list, detail, CRUD) |
| You want the flow readable top-to-bottom | You want to reuse behavior via mixins/inheritance |
| The view doesn't map to Django's generics | You're building repetitive CRUD screens |
Reverse URL Resolution
Hard-coding URLs like /blog/2024/9/15/my-post/ is fragile โ change a route and every link breaks. Instead, refer to routes by their name and let Django build the URL.
# In Python (views, models)
from django.urls import reverse, reverse_lazy
def go(request):
return redirect(reverse("blog:post_detail", kwargs={
"year": 2024, "month": 9, "day": 15, "slug": "my-post",
}))
# In class attributes, use reverse_lazy (evaluated on first use, not import)
class PostDeleteView(DeleteView):
model = Post
success_url = reverse_lazy("blog:post_list")
{# In a template #}
<a href="{% url 'blog:post_detail' year=2024 month=9 day=15 slug='my-post' %}">
My Post
</a>
๐ก reverse vs. reverse_lazy
Use plain reverse() inside functions that run per-request. Use reverse_lazy() for class attributes and other module-level values that are evaluated at import time, before the URLconf is fully loaded.
Even better, give models a get_absolute_url() so "the canonical URL of this object" lives in one place:
from django.urls import reverse
class Post(models.Model):
# ... fields ...
def get_absolute_url(self):
return reverse("blog:post_detail", kwargs={
"year": self.published_at.year,
"month": self.published_at.month,
"day": self.published_at.day,
"slug": self.slug,
})
Hands-on: List & Detail
๐๏ธ Build a list-and-detail pair โ twice
Objective: Implement a post list and a post detail page first as function-based views, then again as generic class-based views, and route both.
Instructions:
- Write
post_listandpost_detailfunction-based views for thePostmodel from the previous lesson. - Add the two routes in
blog/urls.pywith anapp_namenamespace. - Rewrite both as
ListViewandDetailView, and swap the URLconf to use.as_view(). - Confirm both versions render the same pages.
๐ก Hint โ matching a detail by pk
Give the detail route a <int:pk> segment. DetailView looks for pk (or slug) automatically, so you won't even need to override get_object() for the simple case.
โ Sample solution
# blog/views.py โ function-based
from django.shortcuts import render, get_object_or_404
from .models import Post
def post_list(request):
return render(request, "blog/post_list.html",
{"posts": Post.published.all()})
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk, status=Post.Status.PUBLISHED)
return render(request, "blog/post_detail.html", {"post": post})
# blog/views.py โ class-based equivalents
from django.views.generic import ListView, DetailView
class PostListView(ListView):
queryset = Post.published.all()
context_object_name = "posts"
template_name = "blog/post_list.html"
paginate_by = 10
class PostDetailView(DetailView):
model = Post
context_object_name = "post"
template_name = "blog/post_detail.html"
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
# Function-based
path("", views.post_list, name="post_list"),
path("<int:pk>/", views.post_detail, name="post_detail"),
# Class-based (swap in when ready)
# path("", views.PostListView.as_view(), name="post_list"),
# path("<int:pk>/", views.PostDetailView.as_view(), name="post_detail"),
]
Best Practices
โ Do
- Namespace app URLs with
app_nameand reference routes by name. - Use
get_object_or_404for "missing means 404" lookups. - Always follow Post/Redirect/Get after a successful form submission.
- Prefer generic CBVs for standard CRUD; drop to FBVs for the unusual.
- Keep
select_related/prefetch_relatedin mind for list views.
โ ๏ธ Don't
- Hard-code URLs โ use
reverse/{% url %}/get_absolute_url. - Disable CSRF protection to "fix" a form.
- Put access-control mixins after the generic view in the base list.
- Reach for
re_pathwhen a plain path converter would do.
Summary & Quiz
๐ Key Takeaways
- The URLconf routes paths to views; path converters capture and type-check segments.
- Function-based views are explicit and simple; generic CBVs erase CRUD boilerplate.
- The Post/Redirect/Get pattern prevents duplicate submissions โ always redirect after POST.
- Mixins add reusable behavior like login and permission checks (mind the order).
- Reverse resolution keeps links robust โ never hard-code a URL.
๐ฏ Quick Quiz
Question 1: Why should a view redirect after a successful POST (the PRG pattern)?
Question 2: Which generic class-based view is designed to display a paginated list of objects?
Question 3: What does the <int:year> segment in a URL pattern do?
๐ Further Reading
- Django docs โ URL dispatcher
- Django docs โ Class-based views
- Classy Class-Based Views (attribute & method reference)
๐ What's Next?
You can route requests and shape responses. Next, in Django Admin Interface Customization, you'll unlock one of Django's signature features โ a full data-management back office you can tailor to your models with just a few lines.
๐ Well routed!
URLs and views are second nature now. Let's make the admin work for you.