Skip to main content

🧭 Function-Based Views

A view is the piece of Django that turns an incoming web request into a response. Function-based views (FBVs) are the most direct way to write that logic: a plain Python function that receives a request and returns a response. In this lesson you'll learn exactly what happens between the two, and build views that render templates, read the database, and handle form submissions.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain the request β†’ view β†’ response cycle and where views sit in Django's MVT architecture
  • Write function-based views that return plain responses, render templates, and pass model data as context
  • Capture URL parameters and wire views up in urls.py with path converters
  • Handle both GET and POST in one view using the Post/Redirect/Get pattern
  • Apply view decorators and return JSON, and handle errors with get_object_or_404

Estimated Time: 35–45 minutes  β€’  Difficulty: Intermediate

Hands-on: Build a complete function-based blog detail-plus-comment view from scratch.

In This Lesson

What Is a View?

In Django's MVT (Model–View–Template) architecture, a view is the code that decides what should happen when a particular URL is requested. It receives an HttpRequest object, does whatever work the page needs β€” query a model, validate a form, check permissions β€” and returns an HttpResponse (or raises an exception that Django turns into one).

πŸ’‘ A useful analogy: A view is like a restaurant server. It takes the customer's order (the request), relays it to the kitchen and pantry (the models), arranges the result on a plate (the template), and carries it back to the table (the response). The server doesn't cook or store the food β€” it coordinates.

Every view is just a callable that maps a request to a response. Django supports two flavours: function-based views (a plain function, the focus of this lesson) and class-based views (covered next). FBVs are the clearest starting point because everything the view does is visible in one place β€” there is no inherited behaviour hiding off-screen.

The request-to-response cycle through a view A browser request reaches the URL resolver, which dispatches to a view function; the view queries the model and renders a template, then returns an HTTP response to the browser. Browser request URLconf urls.py View your function Model Template Response
Figure 1 β€” The URL resolver picks a view; the view talks to models and templates and returns a response. Your function is the box labelled "View".

Anatomy of a Function-Based View

The smallest possible view takes the request and returns a response:

from django.http import HttpResponse

def hello_world(request):
    return HttpResponse("Hello, World!")

Three things are always true of a function-based view:

  • Its first positional argument is the request β€” an HttpRequest instance carrying the method, headers, query string, POST data, the logged-in user, and more.
  • It returns an HttpResponse (or a subclass like JsonResponse / HttpResponseRedirect), or raises an exception such as Http404.
  • Any extra arguments come from captured URL parameters (you'll see these shortly).

πŸ“– Key Terms

HttpRequest: the object Django builds for every incoming request β€” request.method, request.GET, request.POST, request.user, and so on.

HttpResponse: the object your view returns; it carries the body, a status code, and headers.

Context: the dictionary of data a view hands to a template so the template can fill in the blanks.

Returning raw strings is rare in practice. Real views nearly always render a template or return structured data, which is where the render() shortcut comes in.

Rendering Templates & Model Data

The render() shortcut combines a request, a template name, and a context dictionary into a fully rendered HttpResponse:

from django.shortcuts import render

def homepage(request):
    context = {
        "title": "Welcome to My Site",
        "message": "This is the homepage.",
    }
    return render(request, "blog/homepage.html", context)

The template can then reference {{ title }} and {{ message }}. Most views, though, pull their data from the database first. Because a Django QuerySet is lazy, the query below doesn't hit the database until the template actually iterates over posts:

from django.shortcuts import render
from .models import Post

def post_list(request):
    posts = Post.objects.filter(status="published").order_by("-published_date")
    return render(request, "blog/post_list.html", {"posts": posts})

⚠️ Watch the N+1 query trap

If your template loops over posts and prints {{ post.author.username }}, Django runs a fresh query for every author. Fetch related rows up front with select_related("author") (for foreign keys) or prefetch_related("tags") (for many-to-many). One extra call in the view saves dozens at render time.

URL Parameters & Wiring

A view is useless until a URL points at it. In modern Django you use path() with path converters that both match and type-convert part of the URL, passing it to the view as a keyword argument:

# blog/urls.py
from django.urls import path
from . import views

app_name = "blog"

urlpatterns = [
    path("", views.post_list, name="post_list"),
    path("post/<int:post_id>/", views.post_detail, name="post_detail"),
    path("category/<slug:slug>/", views.category_posts, name="category_posts"),
]

The captured value arrives as an argument of the same name. Use get_object_or_404() so a missing row produces a clean 404 instead of a 500 error:

from django.shortcuts import render, get_object_or_404
from .models import Post

def post_detail(request, post_id):
    post = get_object_or_404(Post, id=post_id, status="published")
    return render(request, "blog/post_detail.html", {"post": post})

πŸ’‘ Common path converters

<int:x> matches digits and yields an int; <slug:x> matches letters, numbers and hyphens; <str:x> matches any non-slash text; <uuid:x> matches a UUID. Setting app_name lets you reverse URLs namespaced as blog:post_detail.

Handling Forms: GET and POST

A single view usually handles both showing an empty form (a GET request) and processing a submitted one (a POST request). The idiomatic shape checks request.method and follows the Post/Redirect/Get pattern β€” after a successful POST you redirect so a page refresh can't resubmit the form:

from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import ContactForm

def contact(request):
    if request.method == "POST":
        form = ContactForm(request.POST)
        if form.is_valid():
            # form.cleaned_data holds validated, typed values
            form.send_email()
            messages.success(request, "Thanks β€” your message was sent.")
            return redirect("blog:contact_success")   # PRG: redirect on success
    else:
        form = ContactForm()   # unbound, empty form for GET

    return render(request, "blog/contact.html", {"form": form})
flowchart TD A[Request to /contact/] --> B{method == POST?} B -- No, GET --> C[Create empty form] C --> D[Render form] B -- Yes, POST --> E[Bind form to request.POST] E --> F{form.is_valid?} F -- No --> D F -- Yes --> G[Save / send] G --> H[Redirect to success page]

When the form is invalid, execution falls through to the final render() β€” but now form is bound and carries error messages, so the template can display them next to the offending fields. Always include {% csrf_token %} inside the <form> in your template; Django rejects unprotected POSTs.

Decorators & JSON Responses

Decorators wrap a view to add reusable behaviour without cluttering its body. A few you'll reach for constantly:

from django.contrib.auth.decorators import login_required, permission_required
from django.views.decorators.http import require_http_methods, require_POST

@login_required
def dashboard(request):
    return render(request, "blog/dashboard.html")

@permission_required("blog.add_post", raise_exception=True)
def create_post(request):
    ...

@require_http_methods(["GET", "POST"])
def edit_profile(request):
    ...

For an API-style endpoint, return JsonResponse instead of HTML. The @require_POST decorator guarantees the view only runs for POST requests:

from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.shortcuts import get_object_or_404
from .models import Post, Like

@login_required
@require_POST
def toggle_like(request, post_id):
    post = get_object_or_404(Post, id=post_id)
    like, created = Like.objects.get_or_create(user=request.user, post=post)
    if not created:
        like.delete()
    return JsonResponse({
        "liked": created,
        "like_count": post.likes.count(),
    })

βœ… Decorator ordering

Decorators apply bottom-up: the one nearest the def runs first. Putting @login_required on top and @require_POST just above the function means Django checks the HTTP method first, then authentication β€” a sensible order.

Hands-on Exercise

πŸ‹οΈ Build a post-detail-with-comments view

Objective: Write one function-based view that displays a blog post and its approved comments, and lets a logged-in visitor submit a new comment via POST.

Requirements:

  1. Match the URL post/<int:post_id>/ and name it post_detail.
  2. Fetch the post with get_object_or_404; on GET, show the post, its approved comments, and an empty CommentForm.
  3. On POST, validate the form, attach the current user and post, save, and redirect back (PRG).
  4. Only allow authenticated users to comment.
πŸ’‘ Hint

Save the comment with commit=False so you can set comment.post and comment.author before the real save(). Guard the POST branch with request.user.is_authenticated, or lean on the @login_required decorator plus a template check.

βœ… Sample solution
from django.shortcuts import render, redirect, get_object_or_404
from .models import Post
from .forms import CommentForm

def post_detail(request, post_id):
    post = get_object_or_404(Post, id=post_id, status="published")
    comments = post.comments.filter(approved=True).select_related("author")

    if request.method == "POST":
        if not request.user.is_authenticated:
            return redirect("login")
        form = CommentForm(request.POST)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.post = post
            comment.author = request.user
            comment.save()
            return redirect("blog:post_detail", post_id=post.id)
    else:
        form = CommentForm()

    return render(request, "blog/post_detail.html", {
        "post": post,
        "comments": comments,
        "form": form,
    })

Notice how the invalid-form case falls through to render() with the bound form, so errors are shown without losing the visitor's input.

🎯 Quick Quiz

Question 1: What must every function-based view accept as its first argument?

Question 2: After a successful POST, why do we redirect() instead of rendering the page directly?

Question 3: Which shortcut returns a clean 404 when a requested object doesn't exist?

Best Practices

βœ… Do

  • Keep views thin β€” push heavy business logic into model methods or a service layer.
  • Use get_object_or_404 / redirect / render shortcuts instead of hand-building responses.
  • Follow Post/Redirect/Get and always include {% csrf_token %} in forms.
  • Optimise queries with select_related / prefetch_related before you hand data to a template.
  • Give every URL a name and reverse it with {% url %} or reverse() rather than hardcoding paths.

⚠️ Don't

  • Don't put try/except-heavy database logic in the view when a shortcut expresses it clearly.
  • Don't modify data in a GET request β€” GET should be safe and repeatable.
  • Don't forget to check permissions; a decorator like @login_required is easy to leave off.
  • Don't return querysets or model instances directly as an API response β€” serialise them (e.g. via JsonResponse or DRF).
When FBVs shine: highly custom, one-off logic β€” a webhook receiver, a dashboard that aggregates several sources, a bespoke multi-form page. When a view is really just "list these objects" or "edit this object," the class-based generics in the next lesson do it in a fraction of the code.

Summary & Quiz

πŸŽ‰ Key Takeaways

  • A view maps a request to a response; an FBV is just a Python function that does so.
  • render() ties a template to a context; model data flows in through that context (mind the N+1 trap).
  • Path converters capture and type URL parts; get_object_or_404 gives clean 404s.
  • One view can handle GET and POST using request.method and the Post/Redirect/Get pattern.
  • Decorators add auth, method restrictions, and caching; JsonResponse powers simple JSON endpoints.

πŸ“š Further Reading

πŸš€ What's Next?

Now that you can hand-write the whole request/response cycle, the next lesson introduces Class-Based Views β€” Django's object-oriented alternative that captures common patterns like "list objects" and "edit an object" so you can build the same features with far less code.

πŸŽ‰ Well done!

You can now trace a request from URL to response and write views that render, query, and handle forms. That's the beating heart of a Django app.