RizTech Academy logo
RizTech Academy
Views, Templates and FormsLesson 5 of 625 min

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

  1. Make patient_create render a page on successful POST (no redirect); submit, then refresh, and watch a duplicate get created. Switch to redirect() and confirm refresh is now safe.
  2. Confirm the valid POST returns 302 and the invalid POST returns 200 (re-render).
  3. 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.
  4. Queue each of the four levels and style them differently by message.tags; confirm the visual difference.
  5. Queue a message but render (instead of redirect) and observe it not appearing as intended; reason about why the display must follow a request.
  6. Redirect to a URL name vs a hard-coded path, then change the route; confirm the named redirect still works.

Official documentation

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