POST/Redirect/GET Pattern
Learning objectives
- You know what the POST/Redirect/GET pattern is and you know what it is used for.
- You know how to use the POST/Redirect/GET pattern in web applications.
When we study our applications with forms, we observe behavior that is likely not wanted. When we send data to the server and refresh the page using the f5
-button (or the refresh button), we notice that the browser wants to send the data to the server again. This is due to the refresh button re-implementing the previously made request. Such behavior is, in general, not wanted.
POST/Redirect/GET
This issue is solved using a web development design pattern called POST/Redirect/GET pattern.
The POST/Redirect/GET pattern refers to a web development design pattern that allows reloading (or refreshing) a page after a form has been submitted without the form data being re-submitted. The pattern is implemented so that when a POST
request is sent to the server, after processing the request, the server responds with a redirect suggestion and a new address. When the browser receives a redirect suggestion as a response to a request, the browser will automatically make a GET
-request to an address given as a part of the response.
This way, the last action will be the GET
-request, and refreshing the page will redo the GET
request instead of a previous POST
request.
In practice, the redirect suggestion is done using the HTTP status code 303
("See other") -- or similar -- and a header Location
with a new address as a value. This automatically prompts the browser to making a GET
request to the address given as the value of the Location
header. Hono's context comes with a redirect
method that simplifies this for us.
Let's illustrate the use of the redirect functionality by making a simple server that always responds with a redirect suggestion. In the example below, we always ask the client to make a new request to the exact same address.
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
const app = new Hono();
app.get("/", (c) => c.redirect("/"));
Deno.serve(app.fetch);
Now, when we launch the server and access it using the browser, we'll be involved in an infinite redirect loop. Fortunately, most browsers can detect this. For example, Chrome states that the page isn't working and points out that there has been too many redirections.

Question not found or loading of the question is still in progress.
POST/Redirect/GET on command line
Let's use the POST/Redirect/GET -pattern as a part of an application used for processing data sent to the server. The first example is rather straightforward. A GET request to /
returns the text "Hello!", while a POST request to /
redirects the client to do a GET request to /
. The application looks as follows.
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
const app = new Hono();
app.get("/", (c) => c.text("Hello!"));
app.post("/", (c) => c.redirect("/"));
Deno.serve(app.fetch);
When we start the application and make a request to it using curl
, we do not see the "Hello!" as a response to the POST request.
curl localhost:8000
Hello!%
curl -X POST localhost:8000
%
This observation stems from curl
not being a real browser. When we look at the headers of the response, we do see the headers that would tell a browser to do a new GET request.
curl -v -X POST localhost:8000
* ...
< HTTP/1.1 302 Found
< location: /
< ...
POST/Redirect/GET on the browser
To text POST/Redirect get on the browser, let's create a simple form for submitting data. For now, we type the form into the code, as we are just trying out the redirect.
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
const app = new Hono();
app.get("/", (c) => c.html(`
<form method="POST" action="/">
<input type="submit"></input>
</form>
`));
app.post("/", (c) => c.redirect("/"));
Deno.serve(app.fetch);
When we run the above application and open it up in the browser, we see a single button. Pressing the button shows us the page with the button, even though pressing the button creates a POST request. This stems from the route handling the POST request returning a redirect, which leads to the browser retrieving the page with a GET request.
Addressing the address controller
Let's take a peek at our address controller that we implemented in the previous part. The code was as follows.
import { Eta } from "https://deno.land/x/eta@v3.4.0/src/index.ts";
import * as addressService from "./addressService.js";
const eta = new Eta({ views: `${Deno.cwd()}/templates/` });
const getAndRenderAddresses = async (c) => {
const data = {
addresses: await addressService.listAddresses(),
};
return c.html(
eta.render("index.eta", data),
);
};
const listAddresses = async (c) => {
return await getAndRenderAddresses(c);
};
const addAddressAndListAddresses = async (c) => {
const body = await c.req.parseBody();
await addressService.addAddress(body);
return await getAndRenderAddresses(c);
};
export { addAddressAndListAddresses, listAddresses };
The address controller above has two routes. The first route is for listing addresses, and the second route is for adding addresses. The first route is a GET route, and the second route is a POST route. The GET route is used for rendering the view, and the POST route is used for adding addresses to the list of addresses as well as rendering the view. There is redundancy in the routes that we can address with the POST/Redirect/GET pattern.
Let's implement the POST/Redirect/GET pattern to the address controller. This is straightforward -- instead of using the getAndRenderAddress
, we use the Context's redirect
-method. There is only one line to change -- the changed line is highlighted below.
import { Eta } from "https://deno.land/x/eta@v3.4.0/src/index.ts";
import * as addressService from "./addressService.js";
const eta = new Eta({ views: `${Deno.cwd()}/templates/` });
const getAndRenderAddresses = async (c) => {
const data = {
addresses: await addressService.listAddresses(),
};
return c.html(
eta.render("index.eta", data),
);
};
const listAddresses = async (c) => {
return await getAndRenderAddresses(c);
};
const addAddressAndListAddresses = async (c) => {
const body = await c.req.parseBody();
await addressService.addAddress(body);
return c.redirect("/");};
export { addAddressAndListAddresses, listAddresses };
With this change, POST requests are redirected to the root path of the application, which shows the form to the user. Let's further adjust the controller a bit -- we can rename the function addAddressAndListAddresses
to addAddress
as the function no longer lists addresses. We can also merge the function getAndRenderAddresses
to listAddresses
, as the function is used in only one place.
import { Eta } from "https://deno.land/x/eta@v3.4.0/src/index.ts";
import * as addressService from "./addressService.js";
const eta = new Eta({ views: `${Deno.cwd()}/templates/` });
const listAddresses = async (c) => {
const data = {
addresses: await addressService.listAddresses(),
};
return c.html(
eta.render("index.eta", data),
);
};
const addAddressAndListAddresses = async (c) => {
const body = await c.req.parseBody();
await addressService.addAddress(body);
return c.redirect("/");
};
export { addAddressAndListAddresses, listAddresses };