URL Routing and Your First View
Coding India · Django Basics
Django Views, Explained Properly
Every Django request ends up at a view — it’s the piece of code that decides what
the user actually sees. This guide takes you from a one-line view function to
class-based views, building a small blog app step by step. No prior Django
experience needed, just a working django-admin startproject setup.
01. Where Views Fit — The MVT Pattern
Django describes itself as following an MVT pattern —
Model, View, Template. If you’ve used MVC frameworks before, this maps almost
directly, but the naming trips a lot of beginners up:
- Model — your data. Python classes that map to database tables.
- View — your logic. Decides what data to fetch and what to do with the request.
- Template — your presentation. HTML files that decide how data looks.
The confusing part: in classic MVC, the “Controller” is the layer that handles
requests — and that’s exactly what Django calls a View. Django’s
“Template” is what other frameworks call a “View”. Once that mapping clicks,
everything else makes sense.
and a response. It can talk to models, render templates, or just return raw
data — the view decides.
02. What Exactly Is a View?
A view in Django is simply a Python function (or class) that
receives an HttpRequest object and returns an
HttpResponse object. That’s its entire contract. Everything
else — querying the database, checking permissions, processing form data — is
just things your view does on the way to building that response.
The HttpRequest object
Django automatically builds this object for every incoming request and passes
it as the first argument to your view (conventionally named request).
Here’s what you’ll reach for most often:
| Attribute | What it gives you |
|---|---|
request.method | The HTTP verb — "GET", "POST", etc. |
request.GET | Query string parameters, e.g. ?page=2 → request.GET['page'] |
request.POST | Form data submitted via POST |
request.user | The logged-in user (or an anonymous user object) |
request.path | The URL path that was requested, e.g. /blog/my-post/ |
request.headers | HTTP headers sent by the client |
request.FILES | Uploaded files from a multipart form |
The HttpResponse object
This is what your view builds and returns. At minimum it has a body
(the content), but it also carries a status code and headers:
HttpResponse("...")— 200 OK with the given contentHttpResponseNotFound("...")— 404HttpResponseRedirect("/some/url/")— 302 redirectJsonResponse({...})— 200 OK with a JSON body and the right content-type header
You can build these directly, but Django gives you shortcut functions
(render, redirect, get_object_or_404)
that wrap them with sensible defaults — we’ll use those throughout this guide.
03. Your First View
Open blog/views.py and write this:
blog/views.py
from django.http import HttpResponse def homepage(request): # 'request' is the HttpRequest object Django passes automatically. # We return a simple HttpResponse with plain HTML content. return HttpResponse("<h1>Welcome to My Blog</h1><p>Hello, Django!</p>")
This is a complete, working view. It receives the request, ignores it (for
now), and returns hard-coded HTML. We’ll replace this with proper templates in
Section 7 — but it’s important to see the bare minimum
first.
be valid. As long as it returns an
HttpResponse (or asubclass of it), Django is happy.
04. Wiring It Up — urls.py
Writing the view function is only half the job. You have to tell Django
which URL should trigger it. Django does this in two steps: an
app-level urls.py, included into the project-level one.
Step 1 — create blog/urls.py
blog/urls.py (new file)
from django.urls import path from . import views # import views.py from the same folder # Each entry maps a URL pattern to a view function. urlpatterns = [ # path('URL pattern', view_function, name='a unique name for this URL') path('', views.homepage, name='homepage'), ]
The empty string '' matches the root of whatever
prefix this app gets mounted at.
Step 2 — include it in the project’s urls.py
mysite/urls.py
from django.contrib import admin from django.urls import path, include # add 'include' urlpatterns = [ path('admin/', admin.site.urls), # For any URL starting at the root, look in blog/urls.py # for further patterns. path('', include('blog.urls')), ]
Run the dev server with python manage.py runserver and
visit http://127.0.0.1:8000/ — you’ll see “Welcome to
My Blog”.
05. Capturing Data From the URL
Real apps need URLs like /blog/my-first-post/ where the
last part changes per page. Angle-bracket syntax in path()
captures that segment and hands it to your view as a keyword argument.
blog/urls.py
from django.urls import path from . import views urlpatterns = [ path('', views.homepage, name='homepage'), # <str:slug> captures a string and passes it as a keyword # argument called 'slug' to the view function. path('blog/<str:slug>/', views.post_detail, name='post_detail'), ]
blog/views.py
from django.http import HttpResponse def homepage(request): return HttpResponse("<h1>Welcome to My Blog</h1>") # Django automatically passes the captured 'slug' as an argument. def post_detail(request, slug): return HttpResponse(f"<h1>You are viewing post: {slug}</h1>")
Built-in path converters
Using the right converter validates the input at the routing layer, before your view even runs:
| Converter | Matches | Example |
|---|---|---|
str | Any non-empty string except / | <str:slug> |
int | Zero or positive integers | <int:pk> |
slug | Letters, numbers, hyphens, underscores | <slug:slug> |
uuid | A formatted UUID | <uuid:id> |
path | Any non-empty string, including / | <path:rest> |
slug over str forslug-based URLs — it rejects characters that would never appear in a real
slug, catching typos earlier.
06. Naming URLs & reverse()
Notice the name= argument on every path()
call. This is one of the most underrated features for beginners to learn early
— it lets you generate URLs by name instead of hard-coding strings.
If the pattern ever changes, every link that uses the name updates automatically.
In Python — reverse()
anywhere in views.py
from django.urls import reverse url = reverse('post_detail', kwargs={'slug': 'my-first-post'}) # url == '/blog/my-first-post/'
In templates — the {% url %} tag
blog/templates/blog/post_list.html
<a href="{% url 'post_detail' slug='my-first-post' %}">Read Post</a>"/blog/..." string into a template or a redirect,stop — use
{% url %} orreverse() instead. Future-you will thank present-youwhen URLs change.
07. Rendering Templates — render()
Returning raw HTML strings from HttpResponse doesn’t
scale. The render() shortcut combines three things in
one call: it loads a template, fills it with context data, and wraps the
result in an HttpResponse for you.
blog/views.py
from django.shortcuts import render, get_object_or_404 from .models import Post def post_detail(request, slug): post = get_object_or_404(Post, slug=slug) return render(request, 'blog/post_detail.html', {'post': post})
The third argument is the context — a dictionary of variables
the template can access. Here, the template gets a variable called
post.
blog/templates/blog/post_detail.html
{% extends "base.html" %}
{% block content %}
<h1>{{ post.title }}</h1>
<p>{{ post.body }}</p>
<p>Published on {{ post.created_at|date:"F j, Y" }}</p>
{% endblock %} get_object_or_404() is another shortcut worth learning
immediately: it fetches an object, and if it doesn’t exist, it raises an
Http404 automatically — instead of an ugly server
error or a manual try/except.
08. redirect(), 404s & JSON Responses
Three more response patterns you’ll use constantly:
redirect() — send the browser elsewhere
blog/views.py
from django.shortcuts import redirect def old_post_url(request, slug): # Send visitors from an old URL pattern to the new one. return redirect('post_detail', slug=slug)
redirect() accepts a URL name (like above), a raw URL
string, or even a model instance with a get_absolute_url() method.
JsonResponse — for APIs and AJAX calls
blog/views.py
from django.http import JsonResponse from django.shortcuts import get_object_or_404 from .models import Post def post_detail_api(request, slug): post = get_object_or_404(Post, slug=slug) return JsonResponse({ 'title': post.title, 'body': post.body, 'created_at': post.created_at.isoformat(), })
JsonResponse sets the
Content-Type: application/json header for you and
serializes the dictionary — no manual json.dumps() needed.
09. Handling Forms — GET vs POST
A single view often needs to do two things: show a form (on a normal
GET request), and process the submitted data (on a
POST request). The standard pattern checks
request.method:
blog/views.py
from django.shortcuts import render, redirect, get_object_or_404 from .models import Post from .forms import CommentForm def post_detail(request, slug): post = get_object_or_404(Post, slug=slug) if request.method == 'POST': form = CommentForm(request.POST) if form.is_valid(): comment = form.save(commit=False) comment.post = post comment.save() return redirect('post_detail', slug=post.slug) else: form = CommentForm() return render(request, 'blog/post_detail.html', { 'post': post, 'form': form, })
Notice the redirect after a successful POST — this prevents
the classic “resubmit form?” warning if the user refreshes the page, and it’s
a pattern called Post/Redirect/Get.
Don’t forget the CSRF token
blog/templates/blog/post_detail.html
<form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit">Add Comment</button> </form>
method="post" needs{% csrf_token %} inside it. Without it, Djangorejects the submission with a 403 Forbidden — this is a security feature, not
a bug.
10. Class-Based Views
Everything so far has been a function-based view (FBV). Django
also lets you write views as classes — a class-based view (CBV).
Instead of one function branching on request.method,
you write a method per HTTP verb: get(),
post(), and so on. Django’s
View base class routes the request to the matching
method automatically (this routing step is called dispatch).
blog/views.py
from django.views import View from django.shortcuts import render, redirect, get_object_or_404 from .models import Post from .forms import CommentForm class PostDetailView(View): def get(self, request, slug): post = get_object_or_404(Post, slug=slug) return render(request, 'blog/post_detail.html', { 'post': post, 'form': CommentForm(), }) def post(self, request, slug): post = get_object_or_404(Post, slug=slug) form = CommentForm(request.POST) if form.is_valid(): comment = form.save(commit=False) comment.post = post comment.save() return redirect('post_detail', slug=post.slug) return render(request, 'blog/post_detail.html', { 'post': post, 'form': form, })
In urls.py, CBVs are wired up with
.as_view() — this converts the class into something
that behaves like a regular view function:
blog/urls.py
path('blog/<slug:slug>/', views.PostDetailView.as_view(), name='post_detail'),
Same behaviour as the FBV from Section 9 — just organised differently. Neither
is “wrong”; CBVs start paying off once you reach for Django’s
generic views, which is exactly what’s next.
11. Generic Class-Based Views
A huge share of views follow the same handful of shapes: list objects, show
one object, create one, update one, delete one. Django ships ready-made
classes for exactly these — you configure them with a few attributes instead
of writing the logic yourself.
blog/views.py
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView from django.urls import reverse_lazy from .models import Post class PostListView(ListView): model = Post template_name = 'blog/post_list.html' context_object_name = 'posts' paginate_by = 10 ordering = ['-created_at'] class PostDetailView(DetailView): model = Post template_name = 'blog/post_detail.html' context_object_name = 'post' slug_field = 'slug' slug_url_kwarg = 'slug' class PostCreateView(CreateView): model = Post fields = ['title', 'slug', 'body'] template_name = 'blog/post_form.html' class PostUpdateView(UpdateView): model = Post fields = ['title', 'body'] template_name = 'blog/post_form.html' class PostDeleteView(DeleteView): model = Post template_name = 'blog/post_confirm_delete.html' success_url = reverse_lazy('post_list')
blog/urls.py
urlpatterns = [
path('', views.PostListView.as_view(), name='post_list'),
path('blog/<slug:slug>/', views.PostDetailView.as_view(), name='post_detail'),
path('blog/new/', views.PostCreateView.as_view(), name='post_create'),
path('blog/<slug:slug>/edit/', views.PostUpdateView.as_view(), name='post_update'),
path('blog/<slug:slug>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
]| Generic view | What it gives you |
|---|---|
ListView | Fetches a queryset, paginates it, renders a template with it in context |
DetailView | Fetches a single object by pk or slug, renders it |
CreateView | Renders a ModelForm, saves it on valid POST |
UpdateView | Same as CreateView, but pre-fills the form from an existing object |
DeleteView | Shows a confirmation page, deletes on POST |
TemplateView | Just renders a template — good for static-ish “about” pages |
exactly, start with the generic version — it’s less code to maintain. The
moment the logic diverges (extra validation, multiple models, custom
redirects), drop down to a plain
View or FBV. Don’tfight the generic view to make it do something it wasn’t built for.
12. Protecting Views
Most apps need some views to be logged-in-only. Django gives you a decorator
for FBVs and a mixin for CBVs — same idea, different syntax.
Function-based view — @login_required
blog/views.py
from django.contrib.auth.decorators import login_required @login_required def post_create(request): # Anonymous users get redirected to LOGIN_URL automatically. ...
Class-based view — LoginRequiredMixin
blog/views.py
from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import CreateView from .models import Post class PostCreateView(LoginRequiredMixin, CreateView): model = Post fields = ['title', 'slug', 'body'] login_url = '/accounts/login/'
The mixin must come first in the parent list — Python resolves
methods left to right, and the mixin needs to intercept the request before
CreateView does its own work.
For more granular control, Django also has
@permission_required (checks a specific permission)
and @require_POST / @require_GET
(rejects a view if the HTTP method doesn’t match — useful for FBVs that should
only ever handle one verb).
13. Common Mistakes & Best Practices
- Keep views thin. If a view is doing complex calculations, pulling that logic into a model method or a separate helper function makes it testable and reusable.
- Always name your URLs and use
reverse()/{% url %}— never hard-code paths. - Use
get_object_or_404instead of a manualtry/except Post.DoesNotExistfor lookups by URL parameter. - Check
request.methodexplicitly in FBVs that handle forms — don’t assume every request is a GET. - Don’t skip
{% csrf_token %}— it’s not optional for POST forms. - Redirect after a successful POST (Post/Redirect/Get) to avoid duplicate form submissions.
- Be deliberate about FBV vs CBV. Generic CBVs save time for standard CRUD; plain functions are often clearer for one-off, unusual logic. Mixing both in one project is completely normal.
14. Quick Reference Cheat Sheet
| Need to… | Use |
|---|---|
| Return plain text/HTML | HttpResponse(content) |
| Render a template with context | render(request, template, context) |
| Fetch-or-404 a model instance | get_object_or_404(Model, **lookup) |
| Send the browser elsewhere | redirect(name_or_url) |
| Return JSON | JsonResponse(dict) |
| Build a URL by name | reverse('name', kwargs={...}) / {% url 'name' %} |
| List of objects | ListView |
| Single object | DetailView |
| Create / update via form | CreateView / UpdateView |
| Delete with confirmation | DeleteView |
| Require login (FBV) | @login_required |
| Require login (CBV) | LoginRequiredMixin (first in bases) |
15. What’s Next
Views are the glue — but they’re only as useful as what’s on either side of
them. The next steps in this series cover:
- Templates — template inheritance, tags, filters, and static files
- Models & the ORM — defining
Post, querysets, migrations - Forms —
ModelForm, validation, widgets
If you’ve followed along, you’ve already got a working
homepage view, a post_detail
view (in both FBV and CBV form), URL capturing, named URLs, and a comment form
— that’s most of what a real Django app’s view layer looks like on day one.