The request lifecycle for backend pages
When a staff user visits an internal page (for example, /dashboard/), Django processes the request through a predictable pipeline. Understanding this flow helps you structure views cleanly and debug issues quickly.
What happens from URL to response
- Browser sends an HTTP request (method like GET/POST, headers, cookies, body).
- Django resolves the URL to a view callable (function-based view or class-based view).
- Middleware runs (before and after the view). Common middleware handles sessions, authentication, CSRF protection, security headers, etc.
- The view executes: reads request data, performs business logic (often via ORM queries), and returns an
HttpResponse(HTML, redirect, 404, etc.). - Template rendering (if used): the view passes context data to a template to produce HTML.
- Response goes back to the browser.
For backend pages, your view is typically responsible for: (1) authorization checks, (2) retrieving data, (3) validating input on POST, and (4) returning an HTML response or redirect.
Views for backend pages: function-based patterns
A view is a Python callable that takes a request and returns a response. For internal dashboards, function-based views (FBVs) are often straightforward and explicit.
A minimal HTML response
from django.http import HttpResponse
def health_check(request):
return HttpResponse("OK")This is useful for simple endpoints, but most backend pages should render templates so you can keep HTML out of Python.
Handling GET vs POST in one view
A common backend pattern is: GET shows a page (form, list, detail), POST processes an action (create/update/delete) and then redirects.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
from django.shortcuts import render, redirect
from django.contrib import messages
def settings_page(request):
if request.method == "POST":
# validate and save settings
# ...
messages.success(request, "Settings updated")
return redirect("settings")
# GET: show current settings
context = {"timezone": "UTC"}
return render(request, "backoffice/settings.html", context)Key idea: after a successful POST, redirect to a GET page (Post/Redirect/Get) to avoid duplicate submissions on refresh.
Core shortcuts you will use constantly
render(): return HTML from a template
render(request, template_name, context) loads a template, renders it with context data, and returns an HttpResponse.
from django.shortcuts import render
def dashboard(request):
context = {
"active_users": 128,
"failed_jobs": 3,
}
return render(request, "backoffice/dashboard.html", context)redirect(): send the user elsewhere
redirect() returns an HTTP 302 (or 301 if configured) to a URL or URL pattern name.
from django.shortcuts import redirect
def go_to_dashboard(request):
return redirect("dashboard") # uses URL nameget_object_or_404(): fetch or return 404
Backend pages often show a detail view for an object by ID. If the object does not exist, you want a proper 404 instead of a server error.
from django.shortcuts import get_object_or_404, render
from .models import Ticket
def ticket_detail(request, ticket_id):
ticket = get_object_or_404(Ticket, pk=ticket_id)
return render(request, "backoffice/tickets/detail.html", {"ticket": ticket})This keeps your code clean and avoids manual try/except blocks for common “not found” cases.
Common backend page patterns
1) List pages (tables, queues, worklists)
List pages show many objects, often with basic filtering and ordering. Keep the view focused on: reading query parameters, building a queryset, and rendering.
from django.shortcuts import render
from .models import Ticket
def ticket_list(request):
status = request.GET.get("status") # e.g. open/closed
qs = Ticket.objects.all().order_by("-created_at")
if status:
qs = qs.filter(status=status)
context = {
"tickets": qs,
"selected_status": status,
}
return render(request, "backoffice/tickets/list.html", context)Practical tips for list pages:
- Prefer query parameters for filters (GET) so the URL is shareable.
- Keep filtering logic readable; if it grows, move it into a helper function or a service layer.
- Don’t do heavy computation in templates; compute derived values in Python and pass them via context.
2) Detail pages (inspect one record)
Detail pages typically load one object and show related data. Use select_related/prefetch_related when you know you’ll need related objects to reduce database queries.
from django.shortcuts import get_object_or_404, render
from .models import Ticket
def ticket_detail(request, ticket_id):
ticket = get_object_or_404(
Ticket.objects.select_related("assignee"),
pk=ticket_id,
)
return render(request, "backoffice/tickets/detail.html", {"ticket": ticket})3) Simple dashboards (metrics + recent activity)
A dashboard view often combines a few small queries and aggregates. The goal is to keep it fast and predictable.
from django.db.models import Count
from django.shortcuts import render
from .models import Ticket
def dashboard(request):
counts = (
Ticket.objects.values("status")
.annotate(total=Count("id"))
.order_by()
)
recent = Ticket.objects.order_by("-created_at")[:10]
context = {
"counts": list(counts),
"recent_tickets": recent,
}
return render(request, "backoffice/dashboard.html", context)Step-by-step: build a “detail + action” backend page (GET shows, POST updates)
This pattern is common in internal tools: a staff user opens a record, then performs an action (change status, assign user, add internal note).
Step 1: Define the view with GET and POST branches
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_http_methods
from .models import Ticket
@require_http_methods(["GET", "POST"])
def ticket_close(request, ticket_id):
ticket = get_object_or_404(Ticket, pk=ticket_id)
if request.method == "POST":
ticket.status = "closed"
ticket.save(update_fields=["status"])
messages.success(request, "Ticket closed")
return redirect("ticket-detail", ticket_id=ticket.id)
return render(request, "backoffice/tickets/confirm_close.html", {"ticket": ticket})Notes:
@require_http_methodsprevents unexpected methods from hitting your logic.- POST performs the mutation and then redirects to the detail page.
messagesis a convenient way to show feedback on the next page load.
Step 2: Create a confirmation template (brief, functional)
<!-- backoffice/tickets/confirm_close.html -->
{% extends "backoffice/base.html" %}
{% block content %}
<h1>Close ticket #{{ ticket.id }}</h1>
<p>Are you sure you want to close “{{ ticket.title }}”?</p>
<form method="post">
{% csrf_token %}
<button type="submit">Confirm close</button>
<a href="{% url 'ticket-detail' ticket.id %}">Cancel</a>
</form>
{% endblock %}Even for internal pages, keep CSRF protection enabled for POST forms.
Templates (briefly): structure, context, and safe output
Template inheritance: consistent layout for internal pages
Backend UIs usually share navigation, a header, and a content area. Template inheritance avoids duplication.
<!-- backoffice/base.html -->
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Backoffice</title>
</head>
<body>
<nav>
<a href="{% url 'dashboard' %}">Dashboard</a>
<a href="{% url 'ticket-list' %}">Tickets</a>
</nav>
<main>
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% block content %}{% endblock %}
</main>
</body>
</html>Child templates extend the base and fill in {% block content %}.
Context data: what you pass from the view
Context is a dictionary of values made available to the template. Keep it explicit and predictable.
def ticket_list(request):
qs = Ticket.objects.all()
return render(request, "backoffice/tickets/list.html", {
"tickets": qs,
"page_title": "Tickets",
})In the template:
<h1>{{ page_title }}</h1>
<ul>
{% for t in tickets %}
<li>
<a href="{% url 'ticket-detail' t.id %}">#{{ t.id }} — {{ t.title }}</a>
</li>
{% empty %}
<li>No tickets found.</li>
{% endfor %}
</ul>Safe output practices: escaping and when to be careful
Django templates escape variables by default, which helps prevent XSS when displaying user-provided content.
- Safe by default:
{{ ticket.title }}is HTML-escaped. - Avoid marking content safe unless you trust it: using
|safecan render raw HTML and introduce XSS if the content is not sanitized. - Prefer plain text fields for internal notes unless you have a clear sanitization strategy.
Example of what to avoid unless the content is sanitized:
{{ ticket.description|safe }}Choosing response types for backend pages
| Goal | Typical response | Common helper |
|---|---|---|
| Show a page | HTML | render() |
| After successful POST | Redirect to a GET URL | redirect() |
| Object not found | 404 page | get_object_or_404() |
| Invalid method | 405 Method Not Allowed | @require_http_methods |
Practical checklist for clean backend views
- Keep views thin: fetch data, validate input, call domain logic, return a response.
- Use GET for reading, POST for changing: avoid mutations on GET endpoints.
- Redirect after POST: prevents accidental resubmission.
- Use
get_object_or_404(): consistent behavior for missing records. - Pass explicit context: templates should not guess or compute heavy logic.
- Rely on default escaping: be cautious with
|safeand HTML-rich fields.