Messages, redirects and the POST-redirect-GET pattern
When a user submits a form successfully, two small things separate a polished application from a confusing one: the browser should not re-submit the form if they refresh, and the user should see a "that worked" confirmation. Django solves both with the POST-redirect-GET pattern and the messages framework. They are simple, but getting them right (and knowing why) is what makes your forms feel correct rather than janky. This lesson covers redirects, the messages framework, and the pattern that ties them together.
The problem: the double-submit on refresh
Imagine patient_create responded to a successful POST by rendering the "success" page directly, with no
redirect. The URL in the browser is still /patients/new/, and the browser remembers it made a POST there.
If the user hits refresh, the browser re-sends the POST — creating the patient again. Refresh twice,
three duplicates. This is a real, classic bug, and users trigger it constantly (refresh is a reflex).
The fix: POST-redirect-GET
The pattern: after a successful POST, do not render — redirect (an HTTP 302) to a GET URL. The browser then makes a fresh GET request to that URL, and that is what it remembers. Now refreshing just re-runs the harmless GET (re-loading the list), never the POST. In Nidaan's create view:
if form.is_valid():
patient = form.save()
return redirect("patient_list") # 302 to a GET URL — not render()
Verified: a valid POST returns 302 and the record is created once; the browser follows the redirect to
the list. redirect() takes a URL name ("patient_list"), a model instance (redirects to its
get_absolute_url()), or a path. Prefer the URL name so the destination survives a route change. The
rule is absolute: a successful POST ends in a redirect, never a rendered page. Only the failed POST
re-renders (to show errors), because there is no new data to protect against re-submission.
The messages framework: telling the user it worked
After the redirect, the new page is the plain list — the user has no confirmation their action succeeded. The messages framework carries a one-off notification across the redirect:
from django.contrib import messages
if form.is_valid():
patient = form.save()
messages.success(request, f"Added {patient.name}.") # queue a message
return redirect("patient_list")
messages.success(request, "…") stashes a message that survives exactly one redirect and is then consumed.
The destination template displays it:
{% for message in messages %}
<p class="msg {{ message.tags }}">{{ message }}</p>
{% endfor %}
Verified: after the redirect, the list page shows "Added Divya." — the message crossed the redirect and
appeared once. Refresh again and it is gone (messages are consumed on display), which is exactly right: the
confirmation shows once, not forever. Put that {% for message in messages %} loop in your base
template so every page can display messages without repeating the code.
The message levels
Messages come in levels, each conventionally styled differently (the message.tags gives you a CSS class):
messages.success(request, "Appointment booked.") # green
messages.info(request, "Report is being generated.") # blue
messages.warning(request, "This slot is nearly full.") # amber
messages.error(request, "Could not reach the lab system.") # red
Use the level that matches the meaning — success for a completed action, error for a failure, warning
for a caveat, info for neutral news. The template styles them by tag, so a user sees the difference
between "done" and "something went wrong". This is a small, cheap way to make an app feel responsive and
communicative.
The messages framework is enabled by default
django.contrib.messages is in INSTALLED_APPS and its middleware in MIDDLEWARE out of the box, and the
messages context variable is available in templates automatically (via the context processor). So you
usually write no setup — just call messages.success(...) in the view and loop messages in the template.
If messages ever fail to appear, the cause is almost always a template that does not render the
{% for message in messages %} loop, or a view that renders instead of redirecting (a message queued and
then not followed by a request that displays it).
Putting it together: the complete, correct form flow
The whole pattern, which every create/update view in Nidaan follows:
def patient_create(request):
if request.method == "POST":
form = PatientForm(request.POST)
if form.is_valid():
patient = form.save()
messages.success(request, f"Added {patient.name}.") # confirm
return redirect("patient_list") # POST-redirect-GET
# invalid: fall through and re-render WITH errors (no redirect, no message)
else:
form = PatientForm()
return render(request, "patients/patient_form.html", {"form": form})
Read the shape: valid POST → message + redirect (so refresh is safe and the user is told); invalid POST → re-render with errors (so the user fixes and resubmits); GET → empty form. This is the template for essentially every form-handling view you will write, and following it is the difference between forms that feel solid and forms that double-submit and leave users guessing.
Check your work
The double-submit problem. Rendering a success page after a POST leaves the browser able to re-send the POST on refresh — creating duplicates.
POST-redirect-GET. After a successful POST, redirect() (302) to a GET URL so a refresh re-runs the
harmless GET, not the POST. Verified: valid POST → 302, created once. Prefer a URL name as the target.
When to render versus redirect. Successful POST redirects; failed POST re-renders (to show errors); GET renders an empty form.
What the messages framework does. Carries a one-off notification across the redirect; queued with
messages.success(request, …), displayed by {% for message in messages %}, and consumed on display.
Verified: the confirmation appeared once after the redirect.
The message levels. success/info/warning/error, styled by message.tags — match the level to
the meaning.
Why it usually needs no setup. contrib.messages and its middleware/context processor are enabled by
default; failures are almost always a missing template loop or a render-instead-of-redirect.
Practice
- Make
patient_createrender a page on successful POST (no redirect); submit, then refresh, and watch a duplicate get created. Switch toredirect()and confirm refresh is now safe. - Confirm the valid POST returns 302 and the invalid POST returns 200 (re-render).
- Add
messages.success(...)before the redirect and the{% for message in messages %}loop to the base template; confirm the message shows once after the redirect and is gone on refresh. - Queue each of the four levels and style them differently by
message.tags; confirm the visual difference. - Queue a message but render (instead of redirect) and observe it not appearing as intended; reason about why the display must follow a request.
- Redirect to a URL name vs a hard-coded path, then change the route; confirm the named redirect still works.
Official documentation
- Django — The messages framework — Levels, tags, and displaying messages.
- Django —
redirect()— What it accepts and returns. - Django —
HttpResponseRedirect(POST-redirect-GET) — The 302 behindredirect().
Next: static files and user uploads.
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