The idea
So far every route answered GET: "give me this page". Websites also need to receive data: a new order, a contact message, a login. That's POST. The method is part of every request, and a route can treat each method differently.
| Method | Meaning | Safe to repeat? | Example |
|---|---|---|---|
GET | read something | yes, changes nothing | view a product |
POST | send data to be processed / create something | no | place an order |
PUT / PATCH | replace / update something | PUT yes, PATCH it depends | update a customer's address |
DELETE | remove something | yes | cancel a draft |
The rule that matters most: GET must never change anything. Browsers prefetch links, search engines crawl them, users refresh pages. If GET /orders/41/cancel cancelled an order, a crawler could cancel your whole shop.
Allowing methods
Toko Kita's contact page has to do two things at the same URL: show the form when someone visits it (a GET), and receive the message when they press Send (a POST). By default a route only accepts GET, so you list the methods it should handle with methods=:
from flask import Flask, request
app = Flask(__name__)
MESSAGES = []
@app.route("/contact", methods=["GET", "POST"])
def contact():
if request.method == "POST":
MESSAGES.append(request.form["message"])
return "Thanks, we got your message.", 201
return "<form method='post'><textarea name='message'></textarea><button>Send</button></form>"
client = app.test_client()
print(client.get("/contact").status_code)
r = client.post("/contact", data={"message": "Do you ship to Medan?"})
print(r.status_code, r.get_data(as_text=True))
print(MESSAGES)
print(client.delete("/contact").status_code)
200
201 Thanks, we got your message.
['Do you ship to Medan?']
405
requestis Flask's object for the current request.request.methodsays which method was used.request.formholds fields posted from an HTML form, like a dict.client.post(path, data={...})sends form data, the same as a browser submitting a form.201 Createdsays "something new was created". A method that isn't allowed gets 405 Method Not Allowed automatically.
Separate functions per method
When the GET and POST logic grow, split them. Flask 2+ has shortcut decorators:
from flask import Flask, request
app = Flask(__name__)
ORDERS = []
@app.get("/orders")
def list_orders():
return {"orders": ORDERS}
@app.post("/orders")
def create_order():
product = request.form.get("product", "").strip()
if not product:
return {"error": "product is required"}, 400
ORDERS.append(product)
return {"created": product, "count": len(ORDERS)}, 201
client = app.test_client()
print(client.post("/orders", data={"product": "Ceramic Mug"}).get_json())
print(client.post("/orders", data={}).status_code)
print(client.get("/orders").get_json())
{'count': 1, 'created': 'Ceramic Mug'}
400
{'orders': ['Ceramic Mug']}
@app.get and @app.post mean @app.route(..., methods=["GET"]) and ["POST"]. Returning a dict sends JSON (more in the API module).
request.form["product"] raises an error (a 400 Bad Request) if the field is missing; request.form.get("product", "") returns a default instead, so you can show a friendly message. Validate everything that comes from a request: it's user input, and users (and attackers) can send anything.
Post/Redirect/Get
After a successful form POST, don't return a page directly. If the user refreshes, the browser asks "resubmit the form?", and a "yes" places the order twice. Instead, redirect to a GET page:
from flask import Flask, request, redirect, url_for
app = Flask(__name__)
ORDERS = []
@app.get("/")
def home():
return f"{len(ORDERS)} orders so far"
@app.post("/orders")
def create_order():
ORDERS.append(request.form["product"])
return redirect(url_for("home"))
client = app.test_client()
r = client.post("/orders", data={"product": "Kettle"})
print(r.status_code, r.headers["Location"])
r = client.post("/orders", data={"product": "Mug"}, follow_redirects=True)
print(r.status_code, r.get_data(as_text=True))
302 /
200 2 orders so far
redirect() returns a 302 with a Location header; the browser then GETs that URL. Refreshing now just reloads the harmless GET page. This pattern is called Post/Redirect/Get (PRG), and nearly every form on the web uses it.
GET reads and is safe to repeat. POST changes things, validates its input, and answers with a redirect (for forms) or a 201 (for APIs).
Status codes for writes
| Situation | Status |
|---|---|
| created something | 201 Created |
| form handled, send the user on | 302 Found (redirect) |
| input missing or invalid | 400 Bad Request |
| method not supported on this URL | 405 Method Not Allowed (automatic) |
Why this matters for clean code
Methods give each URL a clear contract: GET /orders lists, POST /orders creates. Splitting views per method keeps each function single-purpose, validation keeps bad data out, and PRG prevents duplicate orders. These are small habits that separate toy apps from ones that handle real customers.
Recap
GETreads (never changes data);POSTsends data to create or process.methods=["GET", "POST"], or@app.get/@app.post;request.methodtells which.request.formholds posted fields; prefer.get(name, default)and validate.- Return
201for created,400for bad input; disallowed methods get405. - After a form POST,
redirect(url_for(...)): Post/Redirect/Get.