Request & Response Objects
A route's job is more than matching a URL — once a view function is called, it usually
needs to know things about the incoming request itself: what query string was attached,
what data a form submitted, which HTTP method was used. Flask exposes all of that through
a single global object called request. On the way out, a view also has more
than one way to shape what gets sent back to the browser. This lesson covers both sides:
reading the request, and returning a response.
The Global request Object
Import request from the flask package and it becomes your
window into everything about the current HTTP request — query parameters, submitted form
fields, headers, cookies, uploaded files, and the HTTP method itself. It looks like an
ordinary import, but it's actually a context-local proxy: Flask arranges for it to always
point at whichever request is currently being handled, even though your view function
never receives it as a parameter.
# app.py
from flask import Flask, request
app = Flask(__name__)
Reading Query String Parameters
Values appended to a URL after a ?, like ?status=reading, are
read with request.args.get(). Using .get() instead of indexing
with brackets matters — it returns None (or a default you supply) instead of
raising an error when the parameter is missing:
# app.py — filter the Bookshelf book list by status
@app.route('/books')
def book_list():
status = request.args.get('status')
query = Book.query
if status:
query = query.filter_by(status=status)
books = query.all()
return render_template('books/list.html', books=books)
Visiting plain /books leaves status as None and
shows every book. Visiting /books?status=reading sets
status = 'reading' and narrows the list down to books currently being read —
no extra route needed, the same view handles both cases.
Reading Submitted Form Data
When a browser submits an HTML <form> with method="POST",
the fields are available on request.form — same .get() pattern
as query strings, just a different data source:
# app.py — handle a simple "add book" submission
@app.route('/books/add', methods=['POST'])
def add_book():
title = request.form.get('title')
author = request.form.get('author')
new_book = Book(title=title, author=author, status='want_to_read')
db.session.add(new_book)
db.session.commit()
return redirect(url_for('book_list'))
Handling GET And POST In One View
A single view function can serve both the form page and the form submission by branching
on request.method. This keeps the "show the form" and "process the form"
logic for one feature together in one place, instead of splitting it across two routes:
# app.py
@app.route('/books/add', methods=['GET', 'POST'])
def add_book():
if request.method == 'POST':
title = request.form.get('title')
author = request.form.get('author')
new_book = Book(title=title, author=author, status='want_to_read')
db.session.add(new_book)
db.session.commit()
return redirect(url_for('book_list'))
return render_template('books/add.html')
A plain GET request to /books/add falls through to the final
line and shows the empty form. A POST request enters the if
block, saves the new book, and redirects — that redirect is deliberate, and the next
section explains why.
Three Ways A View Can Return A Response
Every view we've written so far has returned either a rendered template or a redirect, but Flask actually accepts several return shapes, and it's worth knowing all three:
# app.py
from flask import make_response
# 1) a plain string — Flask wraps it in a 200 OK response
@app.route('/books/count')
def book_count():
return str(Book.query.count())
# 2) a tuple of (body, status_code)
@app.route('/books/<int:book_id>/rate', methods=['POST'])
def rate_book(book_id):
rating = request.form.get('rating')
if not rating:
return 'Rating is required', 400
book = Book.query.get_or_404(book_id)
book.rating = int(rating)
db.session.commit()
return 'Rating saved', 200
# 3) an explicit Response object via make_response()
@app.route('/books/export')
def export_books():
books = Book.query.all()
response = make_response(render_template('books/export.html', books=books))
response.headers['Content-Disposition'] = 'attachment; filename=bookshelf.html'
response.set_cookie('last_export', 'books')
return response
Reach for a plain string when a route genuinely has nothing more to say than its body —
small utility endpoints, quick debug routes. Reach for the (body, status_code)
tuple whenever the status code itself carries meaning, like a 400 for a missing field.
Reach for make_response() specifically when you need to reach inside the
response after it's built — setting a custom header, attaching a cookie, or forcing a
download — because a plain string or tuple gives you no object to call methods on.
redirect() + url_for(): The Post/Redirect/Get Pattern
Notice that add_book() above ends a successful POST with
redirect(url_for('book_list')) rather than rendering a page directly. This is
the Post/Redirect/Get pattern, and it solves a real problem: if a view
rendered a template as the direct response to a POST, the browser's address
bar would still show that POST request. Hit refresh, and the browser asks
"resubmit this form?" — say yes, and the same book gets added to the shelf a second time.
Redirecting sends the browser a fresh GET request to a new URL instead, so
refreshing the resulting page just re-runs a harmless GET. Always build the
target URL with url_for('view_function_name') rather than hardcoding a path
string — if the route ever changes, every url_for() call pointing at it keeps
working.
A Quick Look Ahead: request.files
Alongside request.args and request.form, Flask also exposes
request.files for anything a form uploads through a
<input type="file"> field — a book cover image, for example. Reading and
saving uploaded files safely involves a few extra steps (filename sanitizing, size limits,
storage location), so it gets its own full treatment in
File Uploads later in this track.