π§ 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.pywith 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.
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
HttpRequestinstance carrying the method, headers, query string, POST data, the logged-in user, and more. - It returns an
HttpResponse(or a subclass likeJsonResponse/HttpResponseRedirect), or raises an exception such asHttp404. - 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})
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:
- Match the URL
post/<int:post_id>/and name itpost_detail. - Fetch the post with
get_object_or_404; on GET, show the post, its approved comments, and an emptyCommentForm. - On POST, validate the form, attach the current user and post, save, and redirect back (PRG).
- 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/rendershortcuts instead of hand-building responses. - Follow Post/Redirect/Get and always include
{% csrf_token %}in forms. - Optimise queries with
select_related/prefetch_relatedbefore you hand data to a template. - Give every URL a
nameand reverse it with{% url %}orreverse()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_requiredis easy to leave off. - Don't return querysets or model instances directly as an API response β serialise them (e.g. via
JsonResponseor 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_404gives clean 404s. - One view can handle GET and POST using
request.methodand the Post/Redirect/Get pattern. - Decorators add auth, method restrictions, and caching;
JsonResponsepowers simple JSON endpoints.
π Further Reading
- Django Docs β Writing views
- Django Docs β render, redirect & get_object_or_404
- Django Docs β View decorators
π 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.