Django Basics #4: URL and Views (FBV)
In #3 Models and ORM basics you got the Post model and data in place. This post exposes that data via URLs and shows how view functions produce responses. Starting with function-based views (FBV) — class-based views (CBV) are in Intermediate #1.
URLconf — mapping URLs to views #
Django’s URL routing lives in urls.py. The urlpatterns list in one file collects the patterns.
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls")),
]from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.post_list, name="post_list"),
path("<int:post_id>/", views.post_detail, name="post_detail"),
path("new/", views.post_new, name="post_new"),
path("<int:post_id>/edit/", views.post_edit, name="post_edit"),
]Three core functions:
path(route, view, name=...)— one URL patterninclude("blog.urls")— splice in another module’s urlpatternsapp_name = "blog"— namespace. Prevents URL name collisions across apps
URL parameters — <int:...>, <slug:...>
#
When you use <type:varname> in the first argument of path(), it’s passed to the view as an argument.
urlpatterns = [
path("<int:post_id>/", views.post_detail, name="post_detail"),
path("category/<slug:slug>/", views.by_category, name="by_category"),
path("year/<int:year>/month/<int:month>/", views.archive, name="archive"),
path("uuid/<uuid:token>/", views.by_token, name="by_token"),
]Built-in converters:
| Type | Matches |
|---|---|
str | Any string excluding the slash (default) |
int | Positive integer |
slug | [-a-zA-Z0-9_]+ (URL-friendly string) |
uuid | UUID format |
path | Any string including slashes |
These parameters arrive as keyword arguments to the view function.
def post_detail(request, post_id):
...
def archive(request, year, month):
...The names must match exactly — if it’s <int:post_id>, the function argument is post_id.
Function-based views (FBV) #
A Django view is a function that takes a request and returns a response. The simplest form:
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, blog!")Two rules — the first argument is request, and the return value is HttpResponse (or a subclass).
The request object
#
What request (more precisely, an HttpRequest instance) carries:
def example(request):
request.method # "GET", "POST", "PUT", "DELETE", ...
request.GET # ?key=value query parameters (QueryDict)
request.POST # POST body (form data)
request.FILES # uploaded files
request.COOKIES # cookie dict
request.session # session dict (#7)
request.user # authenticated user (#7)
request.headers # request headers
request.path # path like "/blog/1/"
...HttpResponse — the most basic response #
from django.http import HttpResponse
def hello(request):
return HttpResponse("Hello!", content_type="text/plain")
def html(request):
return HttpResponse("<h1>Hi</h1>") # default is text/html
def with_status(request):
return HttpResponse("Forbidden", status=403)Use it when you need to control status codes or headers directly.
JsonResponse — JSON response #
from django.http import JsonResponse
def post_list_json(request):
posts = [
{"id": 1, "title": "First post"},
{"id": 2, "title": "Second post"},
]
return JsonResponse({"items": posts})Auto-converts a dict to JSON. Pass safe=False and you can return a list directly. For serious JSON APIs, though, DRF is the right tool.
render — template + context to HTML #
The most-used response helper.
from django.shortcuts import render
from .models import Post
def post_list(request):
posts = Post.objects.filter(is_published=True).order_by("-created_at")
return render(request, "blog/post_list.html", {"posts": posts})Three arguments:
request- template path (relative to the app’s
templates/directory) - context dict (accessed in the template via
{{ posts }})
Creating the template file is detailed in #5. This post focuses on the shape of the view.
get_object_or_404 — 404 if missing #
Post.objects.get(pk=1) raises Post.DoesNotExist when the object isn’t there. Don’t wrap every call in try/except — use get_object_or_404.
from django.shortcuts import get_object_or_404, render
from .models import Post
def post_detail(request, post_id):
post = get_object_or_404(Post, pk=post_id, is_published=True)
return render(request, "blog/post_detail.html", {"post": post})It returns an automatic HTTP 404 when the object isn’t there. You can pass extra conditions like is_published=True too.
There’s a similar get_list_or_404 for QuerySets — 404 if the list is empty.
Branching on request.method — GET / POST #
A common pattern in form views.
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from .models import Post
def post_new(request):
if request.method == "POST":
title = request.POST.get("title", "").strip()
content = request.POST.get("content", "").strip()
if not title:
return render(request, "blog/post_form.html", {"error": "Please enter a title"})
post = Post.objects.create(title=title, content=content, author=request.user)
return HttpResponseRedirect(reverse("blog:post_detail", args=[post.id]))
# GET — empty form
return render(request, "blog/post_form.html")This pattern is tedious to hand-write each time — Django’s Form abstracts it (#6).
Named URLs — why name=... exists
#
Hardcoding URLs is a bad habit. If a path like /blog/1/ changes, you have to update every reference in code and templates. Reference URLs by name.
from django.urls import reverse
reverse("blog:post_list") # "/blog/"
reverse("blog:post_detail", args=[42]) # "/blog/42/"
reverse("blog:post_detail", kwargs={"post_id": 42}) # "/blog/42/"Format: <app_name>:<url_name>. app_name = "blog" in urls.py + path(..., name="post_detail") together produce "blog:post_detail".
In templates: {% url %}
#
<a href="{% url 'blog:post_detail' post.id %}">{{ post.title }}</a>redirect — short version of reverse #
from django.shortcuts import redirect
def post_new(request):
if request.method == "POST":
post = Post.objects.create(...)
return redirect("blog:post_detail", post_id=post.id)
return render(request, "blog/post_form.html")redirect internally combines reverse + HttpResponseRedirect. It accepts URL names, path strings, and model instances (when get_absolute_url is defined on the model).
get_absolute_url — a model’s own URL
#
A method commonly added to models.
from django.db import models
from django.urls import reverse
class Post(models.Model):
title = models.CharField(max_length=200)
# ...
def get_absolute_url(self) -> str:
return reverse("blog:post_detail", args=[self.pk])With this single line:
redirect(post)works automatically- The Admin’s “View on site” link comes alive
- You can use
<a href="{{ post.get_absolute_url }}">in templates
POST and CSRF #
Django requires a CSRF token on every POST request (a security default). Form templates must include {% csrf_token %} — details in #6.
For places where CSRF isn’t needed (like APIs), exclude it via decorator:
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def webhook(request):
...Use csrf_exempt only where it’s truly needed, like an external webhook. Never on user forms.
Restricting HTTP methods — require_http_methods
#
Make explicit which methods a view accepts.
from django.views.decorators.http import require_GET, require_POST, require_http_methods
@require_GET
def post_list(request):
...
@require_POST
def post_delete(request, post_id):
...
@require_http_methods(["GET", "POST"])
def post_new(request):
...For a disallowed method, an automatic 405 Method Not Allowed is returned.
A small combined example #
Four blog views in one place:
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_http_methods
from .models import Post
def post_list(request):
qs = Post.objects.filter(is_published=True).order_by("-created_at")
q = request.GET.get("q")
if q:
qs = qs.filter(title__icontains=q)
return render(request, "blog/post_list.html", {"posts": qs, "q": q or ""})
def post_detail(request, post_id):
post = get_object_or_404(Post, pk=post_id, is_published=True)
return render(request, "blog/post_detail.html", {"post": post})
@login_required
@require_http_methods(["GET", "POST"])
def post_new(request):
if request.method == "POST":
title = request.POST.get("title", "").strip()
content = request.POST.get("content", "").strip()
if title and content:
post = Post.objects.create(
title=title,
content=content,
author=request.user,
)
return redirect("blog:post_detail", post_id=post.id)
return render(request, "blog/post_form.html")
@login_required
@require_POST
def post_delete(request, post_id):
post = get_object_or_404(Post, pk=post_id, author=request.user)
post.delete()
return redirect("blog:post_list")@login_required redirects unauthenticated users to the login page — covered in #7.
FBV vs CBV — a preview #
Django also offers class-based views (CBV).
from django.views.generic import ListView, DetailView
class PostListView(ListView):
model = Post
template_name = "blog/post_list.html"
context_object_name = "posts"
class PostDetailView(DetailView):
model = PostCBVs compress repeated patterns into a class, but FBVs are more intuitive at first. FBVs alone are enough for small projects. The depth of CBVs is treated separately in Intermediate #1.
Recap #
What this post nailed down:
- URLconf — modularize with
path(),include(),app_name - URL parameters —
<int:>,<slug:>,<uuid:>,<path:> - FBV —
def view(request, ...) -> HttpResponse request.method,request.GET,request.POST,request.userHttpResponse,JsonResponse,renderget_object_or_404— 404 automatically when missing- Named URLs —
app:name+reverse/{% url %}/redirect get_absolute_urlconvention- CSRF is a default —
{% csrf_token %}is required in forms require_GET / POST / http_methodsfor method restriction- CBVs in Intermediate #1
In the next post (#5 Templates and static files), you’ll get to the place where the render(...) from the view actually becomes HTML — template syntax, inheritance, static files (CSS, JS, images).