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.

Note: These examples are correct, working Flask code, but this website's own project doesn't run a Flask server, so none of it executes live here — read each one alongside the explanation, then run it in your own project to see it in action.

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.

Next up: Not every request goes smoothly — a missing book, a bad lookup, an unexpected exception. Error Handling covers replacing Flask's default error pages with your own, and reacting cleanly when something goes wrong.