Code Review Videos > JavaScript / TypeScript / NodeJS > Post Redirect Get Pattern Example

Post Redirect Get Pattern Example

In this post we are going to cover the Post Redirect Get pattern using NodeJS, Express, TypeScript, and for rendering things, Nunjucks and the Gov UK front end. This pattern is generic to any MVC implementation, and the concepts are very easily adapted.

The Post Redirect Get pattern is for handling form submissions in web applications.

All too frequently I see people rendering content in their POST handler.

When rendering content in POST requests, form submissions method can lead to unexpected behaviour, such as duplicate form submissions when a user refreshes the page.

To address this issue, we can employ the Post Redirect Get (PRG) pattern.

Find the code for this post over on GitHub.

What is the Post Redirect Get (PRG) Pattern?

The PRG pattern is a web development design pattern that ensures that a form submission page can be safely refreshed or bookmarked without causing unintended consequences.

Instead of immediately displaying the form’s response, the server redirects the user to a corresponding GET route.

This effectively clears the form data from the browser and prevents accidental resubmissions.

Why Do We Use the Post Redirect Get Pattern?

The PRG pattern is widely adopted for several reasons:

  1. Prevents Duplicate Form Submissions: By redirecting the user after form submission, the pattern ensures that the form data is not resubmitted with a subsequent refresh. This safeguards against unintended actions, such as multiple form submissions or something horrendous like double charging for purchases.
  2. Enhances User Experience: A redirect after form submission provides a cleaner and more user-friendly experience. Users don’t have to worry about accidentally submitting the form again, and they could even bookmark the confirmation page, if they so desire.
  3. Simpler Developer Experience: We’re constantly being told “don’t repeat yourself”, so why would you render once in the GET, and then render the same thing again in a POST? This is a nice way to reduce bugs.

What We Will Build

We’re going to create a very simple little website that has two journeys.

There will be a starting page to select your journey:

post redirect get journey start page

You can then pick an example.

Both use the same page template, because the important part of Post Redirect Get actually happens server side.

Whichever link you choose, you will see something like this:

post redirect get main page

This page has a few elements.

We will print out the name of the controller action that rendered this view. This will help you see exactly what is happening during this flow. That’s the bit that says:

This content is rendered from: “GET /without-prg”

Then we have a set of four radio buttons. I stole this wholesale from the Gov UK components website. It really doesn’t matter what our form actually does, just that we can submit something.

We will then show how many submissions we have currently handled. This is a bit of a hack, not something you would ever actually add to a front end, but again, it is there to more clearly illustrate what is happening behind the scenes.

And finally we have the “Save and continue” button, which submits the form.

Important to this flow is error handling, as that is where this pattern solves a core issue:

post redirect get example with errors rendered

Finally if you do select an option and submit, it takes you to a ‘Check your answers’ page.

There’s no massive purpose to this, I just needed a nice place to send you on to, if everything is fine with the submission:

post redirect get happy outcome

This is a very standard workflow, if you have ever worked on a Gov UK project.

If not, it doesn’t matter, it isn’t about the Gov UK look-and-feel, it’s about what is happening behind the scenes.

So let’s look at that, right now.

Example Implementation

There are some elements that are common to both approaches. We shall set those up first.

This project is based off my Gov UK project starting point example, so I won’t go into depth on everything.

Here’s the template we will use:

{% extends "layout.njk" %}

{% from "govuk/components/radios/macro.njk" import govukRadios %}
{% from "govuk/components/button/macro.njk" import govukButton %}
{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
{% from "govuk/components/input/macro.njk" import govukInput %}

{% block pageTitle %}Where do you live?{% endblock %}

{% set hasErrors = errors.length > 0 %}

{% block content %}

  {% if hasErrors %}
    {{ govukErrorSummary({
      titleText: "There is a problem",
      errorList: [
        {
          text: "Select where do you live",
          href: "#whereDoYouLive"
        }
      ]
    }) }}
  {% endif %}

  <h1 class="govuk-heading-l">Post Redirect Get Example</h1>

  <p class="govuk-body">We are quite nosy and want to know where abouts you are currently living.</p>

  <p class="govuk-body">This content is rendered from: <span class="govuk-!-font-weight-bold">"{{ renderedFrom }}"</span></p>

  <form method="post">
    {{ govukRadios({
      name: "whereDoYouLive",
      fieldset: {
        legend: {
          text: "Where do you live?",
          isPageHeading: true,
          classes: "govuk-fieldset__legend--m"
        }
      },
      items: [
        {
          value: "england",
          text: "England"
        },
        {
          value: "scotland",
          text: "Scotland"
        },
        {
          value: "wales",
          text: "Wales"
        },
        {
          value: "northern-ireland",
          text: "Northern Ireland"
        }
      ],
      errorMessage: hasErrors and {
        text: "Select where do you live"
      }
    }) }}

    {{ govukInput({
      id: "form-submission-count",
      name: "formSubmissionCount",
      value: formSubmissionCounter | string,
      disabled: true,
      label: {
        text: "How many times has this form been submitted?",
        classes: "govuk-label--m",
        isPageHeading: true
      },
      suffix: {
        text: "submissions"
      },
      classes: "govuk-input--width-5",
      spellcheck: false
    }) }}

    {{ govukButton({
      text: "Save and continue"
    }) }}
  </form>
{% endblock %}Code language: Twig (twig)

OK, so a few bits to call out:

{% set hasErrors = errors.length > 0 %}Code language: Twig (twig)

The entire error summary component, and the error styling for the radio buttons component are conditionally rendered if we determined the previous form submission contained errors.

Rather than duplicate the logic, I have extracted out the check to a template variable, which I can then reference more easily:

  {% if hasErrors %}
    {{ govukErrorSummary({
      titleText: "There is a problem",
      errorList: [
        {
          text: "Select where do you live",
          href: "#whereDoYouLive"
        }
      ]
    }) }}
  {% endif %}Code language: Twig (twig)

I’ve hardcoded the error here, which you probably wouldn’t do in a real project. But it keeps things simple.

Next we want to output which controller function rendered the view:

 <p class="govuk-body">This content is rendered from: <span class="govuk-!-font-weight-bold">"{{ renderedFrom }}"</span></p>Code language: Twig (twig)

We will pass in a renderedFrom variable from our controller actions.

  <form method="post">
    {{ govukRadios({
      name: "whereDoYouLive",
      items: [
        ...
      ],
      errorMessage: hasErrors and {
        text: "Select where do you live"
      }
    }) }}Code language: Twig (twig)

A few things are happening here.

<form method="post">Code language: HTML, XML (xml)

In a HTML form, if you do not specify the action, then the form will POST back to the same route it rendered on.

As such we will have both a handler function for both routes in our controller:

  • router.get("/without-prg"...
  • router.post("/without-prg"...

The name: "whereDoYouLive" property is what the name of our data will be on the req.body when we handle the POST submission.

And lastly we have that conditional errorMessage check.

Next, we have a really basic “Check your answers” page:

{% extends "layout.njk" %}

{% from "govuk/components/back-link/macro.njk" import govukBackLink %}
{% from "govuk/components/summary-list/macro.njk" import govukSummaryList %}
{% from "govuk/components/button/macro.njk" import govukButton %}

{% block beforeContent %}
  {{ govukBackLink({
    text: "Back",
    href: "/"
  }) }}
{% endblock %}

{% block pageTitle %}Check Your Answers<{% endblock %}

{% block content %}
  <h1 class="govuk-heading-l">Check your answers</h1>

  {{ govukSummaryList({
    rows: [
      {
        key: {
          text: "Where do you live?"
        },
        value: {
          text: whereDoYouLive
        },
        actions: {
          items: [
            {
              href: "/",
              text: "Change",
              visuallyHiddenText: "change"
            }
          ]
        }
      }
    ]
  }) }}

{% endblock %}
Code language: Twig (twig)

There’s nothing exciting happening here.

The only thing of interest is the highlighted line, where we render whatever value we got from the form submission. Really this bit is not important for this concept. It just brings everything together.

I am aware both the Back button, and the Change links are going back to the site root. This is to save adding complexity.

Those are the views. Now the common controller actions:

import express from "express";

const router = express.Router();

// Initialise counter
let formSubmissionCounter = 0;

router.get("/", (req: express.Request, res: express.Response) => {
  req.session.whereDoYouLive = undefined;
  formSubmissionCounter = 0;
  res.render("index.njk");
});

router.get(
  "/check-your-answers",
  (req: express.Request, res: express.Response) => {
    res.render("check-your-answers.njk", {
      whereDoYouLive: req.session.whereDoYouLive
    });
  },
);

export default router;Code language: TypeScript (typescript)

In order to illustrate one of the main problems that Post Redirect Get is going to solve, we will create a simple counter variable that keeps track of the number of times the form has been submitted.

When you visit the / route, the counter is reset and any previously submitted data is unset from the session storage. That’s how we get back to a clean setup.

And then the "/check-your-answers" route passes through whatever was submitted and is currently stored on the session for display. I am aware you can visit this page without anything being set and see nothing… it’s a bug, but it’s outside the scope of this example.

import "express-session";

declare module "express-session" {
  interface SessionData {
    whereDoYouLive: string | undefined;
  }
}Code language: TypeScript (typescript)

Lastly we have a little bit of essential code to tell TypeScript what is and isn’t allowable on our Session / req.session.

You can read more about that here.

OK, all the common code covered, let’s dive into the implementation that doesn’t use Post Redirect Get.

Example Implementation Without PRG

We will start by creating the website in what I consider the buggy state.

This will work, but it will exhibit all the problems that Post Redirect Get more gracefully solves.

Here are the two route handlers:

router.get("/without-prg", (req: express.Request, res: express.Response) => {
  res.render("where-do-you-live.njk", {
    formSubmissionCounter,
    renderedFrom: "GET /without-prg"
  });
});

router.post("/without-prg", (req: express.Request, res: express.Response) => {
  req.session.whereDoYouLive = req.body.whereDoYouLive;

  formSubmissionCounter++;

  if (!req.body.whereDoYouLive) {
    return res.render("where-do-you-live.njk", {
      errors: ['had errors'],
      renderedFrom: "POST /without-prg",
      formSubmissionCounter,
    });
  }

  res.redirect("/check-your-answers");
});
Code language: TypeScript (typescript)

If you were a little sick in your mouth 🤢 then we would make good friends in real life.

OK, so what’s going on here?

We will cover the GET first.

We render out the common "where-do-you-live.njk" template, passing in two variables:

  • formSubmissionCounter: should be 0 on first visit.
  • renderedFrom: "GET /without-prg": that’s a static bit of content to help visualise exactly what just rendered the view you requested.

We already saw the output, but let’s see it again:

post redirect get without example output

All good.

Now, the POST handler:

router.post("/without-prg", (req: express.Request, res: express.Response) => {
  req.session.whereDoYouLive = req.body.whereDoYouLive;

  formSubmissionCounter++;

  if (!req.body.whereDoYouLive) {
    return res.render("where-do-you-live.njk", {
      errors: ['had errors'],
      renderedFrom: "POST /without-prg",
      formSubmissionCounter,
    });
  }

  res.redirect("/check-your-answers");
});
Code language: TypeScript (typescript)

As covered earlier, when the form submission is received, the expected value from the radio buttons should be made available to us (via the body-parser) on req.body.whereDoYouLive.

It will either be a string – one of the values from the radio buttons – or it will be undefined.

Ahoy, another bug creeps in here. If you are a nefarious hacker type 🥷 then yes, you could use Postman or whatever to send in any old crap here. Of course you would do nicer validation in real life. We’re keeping things simple here.

Next, we immediately increment the formSubmissionCounter variable by one.

Now, one of two things happens next.

If we got a good form submission, we can skip the if(!req.body...) stuff entirely, and just redirect to the next page in the flow. All good ✅

If req.body.whereDoYouLive is undefined / empty, we will re-render the page.

This should be 🚨 setting off alarms 🚨 like crazy in your head.

post redirect get rendered from post error

The obvious problem here is that we have just duplicated the render call we made in the get handler. If we ever add a variable to that template, we now need to update both functions to ensure they are in sync. That’s a potential source of bugs for sure. I hope you have good tests. In my experience when I see this pattern, I tend not to find tests…

If I click “Save and continue” now without making a selection, the counter keeps incrementing:

I clicked twice more, and now the counter is showing a value of 3 submits.

If I click the browser refresh button, I get a potentially confusing pop-up:

post redirect get confirm resubmit popup

You and I might understand this, but what about a none-technical user? That’s a big deal on Gov UK projects for sure.

And if I Continue here, I still re-render from the POST handler and increment the counter:

post redirect get after browser fresh

Imagine something else is happening in the POST handler. Maybe a database call. Maybe multiple database calls. An attempt to make a card charge.

It’s not good.

The only way to ‘fix’ this is one of:

  • Physically revisit the page by highlighting the URL and pressing return to re-trigger a new GET
  • Go back to the root / page which clears everything
  • Do a valid form submit

Of course we know this because we can visually see it. Would you have known the POST handler was responsible for the render, had it not been visibly displayed?

In summary, without using Post Redirect GET, the flow is as follows:

  • When the user submits the form (/without-prg route), the formSubmissionCounter is still incremented.
  • The server renders the /where-do-you-live page directly, without redirecting.
  • If the user now refreshes the page, the browser re-submits the form data along with the counter incrementing logic, causing the counter to increase again.
  • This leads to unintended behaviour where a simple page refresh results in additional increments to the counter.

Enough of this, let’s fix it.

Example Implementation With Post Redirect Get

OK, so the one thing to call out before we go through the revised implementation is that the code becomes slightly more confusing at a glance.

Logically, what we have just seen makes some good sense.

We render out a page with a form.

The user can submit the form.

If there are some errors, we show them.

Otherwise we redirect to the next page.

In order to use the GET route handling function for initial display, and for rendering errors, we need to make things that little bit more complex.

Let’s see why:

router.get("/with-prg", (req: express.Request, res: express.Response) => {
  res.render("where-do-you-live.njk", {
    errors: req.flash("errors"),
    renderedFrom: "GET /with-prg",
    formSubmissionCounter,
  });
});
Code language: TypeScript (typescript)

This is almost identical to the route handler for router.get("/without-prg"..., but now we have this errors variable.

Yet, this is seemingly appearing from nowhere.

Well, yes.

That is definitely the point of confusion I’ve experienced showing this pattern to other developers.

It’s counter intuitive. The errors come from the POST handler, but the POST handler will redirect back to the GET handler if there is an error.

That sounds like as good a time as any to cover the POST handler then:

router.post("/with-prg", (req: express.Request, res: express.Response) => {
  if (!req.body.whereDoYouLive) {
    req.flash("errors", "some error message here");
    return res.redirect("/with-prg");
  }

  formSubmissionCounter++;

  req.session.whereDoYouLive = req.body.whereDoYouLive;

  res.redirect("/check-your-answers");
});Code language: PHP (php)

Now, in the real world you would almost certainly look to extract the validation logic out somewhere else.

This might be to a middleware, or some helper function. That’s beyond the scope of this post.

post redirect get render with PRG

The point is, however, that we do some kind of validation and if there are errors, put the error messages on some kind of short lived storage mechanism. The ‘flash’ is perfect for this.

The flash is a special area of the session used for storing messages. Messages are written to the flash and cleared after being displayed to the user. The flash is typically used in combination with redirects, ensuring that the message is available to the next page that is to be rendered.

docs for connect-flash

This is how we neatly solve the otherwise quite puzzling problem 🤔 of how can we save the validation result and share it between two routes? Well, we do the validation in the POST handler (as you would expect), then save the result to that user’s session.

A flash message is a one-shot thing. You save something to it. Then can use it exactly once after. This means if you do something like:

router.get("/with-prg", (req: express.Request, res: express.Response) => {

  // we 'use' the flashbag contents here
  console.log(`what's in the flash bag?', req.flash("errors"));

  res.render("where-do-you-live.njk", {
    // then this will now be empty
    errors: req.flash("errors")
  });
});Code language: TypeScript (typescript)

It really is a one time use. It can be a little irritating if you are used to console.log style debugging, as it will print out to your console, and then not print out where you expect it.

Anyway, that is how we side step around the potentially confusing problem of how to share a validation result between different route handlers.

The rest is pretty self explanatory I think, but we will cover it anyway.

We update the counter to indicate that a state change – some DB call(s) or other side effect – should only really run after the validation has taken place, and rather crucially, passed.

After this there is a bit of house keeping. We set the value on the session storage, and redirect to the “Check your answers” page.

What’s interesting now is that the form submissions is always at zero, even when the form errors:

post redirect get with errors

The form is always rendered by the GET handler. There is no other way.

The validation logic ensures the state is never updated, unless the form submission is valid. This is a bit of a cheat as I am aware you could easily move the formSubmissionCounter++; call after the validator logic in the previous flow, but that would spoil the impact 🙂

You can happily refresh this page now and it won’t throw up a “strange” pop up message.

Of course there are some bugs here. This isn’t a true and proper solution. The session is not being properly cleared on a valid submit, so if you go Back you can see increment the counter with another valid submission.

But hopefully the underlying concept makes some sense. And, in my humble opinion, the method that uses Post Redirect Get is actually simpler. It’s not like this pattern is for one controller route. You will use this all over the site, so that one little quirk about using the flash isn’t more than a one time thing to understand.

These are really just my opinions. The fact this is a common pattern for the web should hopefully steer you towards this, or some variant rather than the original approach.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.