Django Basics #4: URL and Views (FBV)

7 min read

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.

config/urls.py — top level
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls")),
]
blog/urls.py — per-app
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 pattern
  • include("blog.urls") — splice in another module’s urlpatterns
  • app_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.

path converters
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:

TypeMatches
strAny string excluding the slash (default)
intPositive integer
slug[-a-zA-Z0-9_]+ (URL-friendly string)
uuidUUID format
pathAny string including slashes

These parameters arrive as keyword arguments to the view function.

blog/views.py
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:

blog/views.py — the smallest view
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:

commonly used request attributes
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 #

HttpResponse
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 #

JsonResponse
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.

render
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:

  1. request
  2. template path (relative to the app’s templates/ directory)
  3. 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.

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.

GET / POST branch
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.

reverse
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 %} #

In a template
<a href="{% url 'blog:post_detail' post.id %}">{{ post.title }}</a>

redirect — short version of reverse #

redirect
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.

blog/models.py
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:

csrf_exempt (use with care)
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.

Restrict methods
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:

blog/views.py — combined
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).

🚫 Not covered yet — CBV preview
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 = Post

CBVs 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.user
  • HttpResponse, JsonResponse, render
  • get_object_or_404 — 404 automatically when missing
  • Named URLs — app:name + reverse / {% url %} / redirect
  • get_absolute_url convention
  • CSRF is a default — {% csrf_token %} is required in forms
  • require_GET / POST / http_methods for 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).

X