Function-based views and URLs
A view is the code that runs when a URL is requested — the step 4 in the request cycle where your logic lives. Django offers two styles: function-based views (a plain Python function) and class-based views (the next-but-one lesson). We start with function views because they are explicit — you can see exactly what happens — and because everything a class-based view does, a function view does visibly. This lesson writes real views for Nidaan and wires them to URLs.
A view is a function: request in, response out
The contract from the request-cycle lesson, made concrete. A view takes an HttpRequest and returns an
HttpResponse:
# patients/views.py
from django.shortcuts import render
from .models import Patient
def patient_list(request):
city = request.GET.get("city", "Pune") # read a query parameter, default "Pune"
patients = Patient.objects.filter(city=city).order_by("name")
return render(request, "patients/patient_list.html", {"patients": patients, "city": city})
Three things happen: read input from the request, query the database with the ORM, and return a rendered
response. request.GET.get("city", "Pune") reads ?city=… from the URL (with a default). render(...)
is the workhorse — it takes the request, a template name, and a context dictionary, renders the template
with that context, and returns an HttpResponse. Verified: this view returns HTTP 200 with the patients
listed. render is the helper you will use in almost every view that produces a page.
Wiring a view to a URL
A view does nothing until a URL points at it. Each app owns a urls.py:
# patients/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("", views.patient_list, name="patient_list"),
path("new/", views.patient_create, name="patient_create"),
]
And the project's root urls.py includes the app's, under a prefix:
# config/urls.py
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("patients/", include("patients.urls")), # /patients/ and /patients/new/
]
So patient_list answers /patients/ and patient_create answers /patients/new/. Each path() takes a
route, a view, and — importantly — a name. The name lets you refer to the URL by name elsewhere
(redirect("patient_list"), {% url "patient_list" %} in templates) instead of hard-coding the path, so
if the route changes, the name still resolves. Always name your URLs; hard-coded paths scattered through
code and templates are a maintenance trap.
Capturing parameters from the URL
Many views need a value from the path — a specific patient's id, say. Path converters capture it:
# patients/urls.py
path("<int:patient_id>/", views.patient_detail, name="patient_detail"),
# patients/views.py
from django.shortcuts import get_object_or_404
def patient_detail(request, patient_id): # the captured value arrives as an argument
patient = get_object_or_404(Patient, id=patient_id)
return render(request, "patients/patient_detail.html", {"patient": patient})
<int:patient_id> matches an integer in the URL and passes it to the view as patient_id. So
/patients/7/ calls patient_detail(request, patient_id=7). The converter (int, str, slug, uuid)
both matches and types the value. And get_object_or_404 is the idiom you will reach for constantly: fetch
the object, or raise a 404 if it does not exist — far better than a bare Patient.objects.get(id=…), which
would raise a DoesNotExist (a 500 error, the wrong response) when the id is missing. A missing record
should be a 404, and get_object_or_404 gives you that for free.
Handling GET and POST in one view
A view that shows a form and processes its submission handles both HTTP methods, branching on
request.method:
def patient_create(request):
if request.method == "POST":
form = PatientForm(request.POST) # bind the submitted data
if form.is_valid():
patient = form.save()
return redirect("patient_list") # success: redirect
else:
form = PatientForm() # GET: an empty form
return render(request, "patients/patient_form.html", {"form": form})
The pattern (which the forms and redirect lessons detail): on GET, show an empty form; on POST, validate
the submitted data and either save-and-redirect or re-render with errors. Verified: a valid POST returns 302
(redirect) and creates the record; an invalid POST returns 200 (re-render) and does not. request.method
branching is how one view serves both the display and the submission of a form.
Returning other kinds of response
render and redirect cover most pages, but a view can return any HttpResponse:
from django.http import HttpResponse, JsonResponse
def ping(request):
return HttpResponse("ok") # plain text/HTML
def stats(request):
return JsonResponse({"patients": Patient.objects.count()}) # JSON
JsonResponse serialises a dict to JSON (useful for simple endpoints, though the API module uses Django
REST Framework for real APIs). The point stands: a view's job is to return an HttpResponse, and Django
gives you helpers for the common kinds — HTML via render, a redirect via redirect, JSON via
JsonResponse, or a hand-built HttpResponse for anything else.
Check your work
What a view is. A function taking HttpRequest and returning HttpResponse; it reads input, queries via
the ORM, and returns a response. Verified: patient_list returns 200 with the data.
What render does. Takes the request, a template name, and a context dict; renders and returns an
HttpResponse — the workhorse for pages.
Why URLs get a name. So you refer to them by name (redirect(...), {% url %}) instead of hard-coding
paths — routes can change, names still resolve.
Capturing path parameters. <int:patient_id> matches and types a URL segment, passed to the view as an
argument; get_object_or_404 fetches it or returns a proper 404 (not a 500).
GET/POST in one view. Branch on request.method: GET shows an empty form, POST validates and either
redirects (verified 302, created) or re-renders with errors (verified 200, not created).
Other responses. A view can return any HttpResponse — JsonResponse for JSON, a plain HttpResponse,
or a redirect.
Practice
- Write
patient_listand wire it at/patients/; load it and confirm the list renders (200). - Add a
nameto each URL, then use{% url "patient_list" %}in a template instead of the literal path; confirm it still works, then change the route and confirm the name still resolves. - Add
patient_detailwith<int:patient_id>andget_object_or_404; visit a real id and a missing one, and confirm you get the detail page and a proper 404 respectively. - Replace
get_object_or_404withPatient.objects.get(...)for a missing id and observe the 500 error; reason about why 404 is the correct response. - Write
patient_createhandling GET and POST; confirm a valid POST redirects (302) and an invalid one re-renders (200). - Add a
statsview returningJsonResponsewith a count; load it and read the JSON.
Official documentation
- Django — Writing views — Function views and responses.
- Django — URL dispatcher —
path, converters,name, andinclude. - Django —
renderandget_object_or_404— The view shortcuts.
Next: templates and template inheritance.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship