Flask Routing and Views

Route decorators, path converters, HTTP methods, the request object, and returning JSON.

Defining routes with @app.route()

Every URL your Flask app responds to is registered with the @app.route() decorator on a plain Python function (commonly called a view function):

Python
from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "Home page"

@app.route("/about")
def about():
    return "About page"

Flask matches the incoming request path against every registered route and calls the matching view function. There's no separate routing file to maintain — the route and the code that handles it live together.

Path parameters and converters

A route segment wrapped in <...> captures part of the URL and passes it to the view function as an argument. By default it's captured as a string, but Flask ships several converters that validate and cast the segment for you:

Python
@app.route("/users/<int:user_id>")
def show_user(user_id):
    # user_id is already a Python int here, not a string
    return f"User #{user_id}"
Converter Matches
(none) Any text without a slash (default, returns str)
int Positive integers, returns int
float Positive floating-point values
path Like the default, but also accepts slashes
uuid A UUID string, returns a UUID object

If a request comes in for /users/abc against the <int:user_id> route above, Flask returns a 404 automatically — the segment simply doesn't match an int, so that route isn't considered a match at all.

Restricting HTTP methods

By default, a route only responds to GET (and Flask automatically handles HEAD and OPTIONS for you). Pass methods to accept others:

Python
from flask import request

@app.route("/submit", methods=["GET", "POST"])
def submit():
    if request.method == "POST":
        return "Form submitted!"
    return "Show the form"

Visiting /submit in a browser sends a GET; submitting an HTML <form method="post"> to the same URL sends a POST — the same view function branches on request.method to handle both.

The request object

Flask exposes the current, in-flight HTTP request through a global-looking object you import directly — request. It behaves correctly per-request even though it's imported once, because Flask manages it internally as a context-local value scoped to the current request.

Python
from flask import request

@app.route("/search")
def search():
    query = request.args.get("q", "")     # ?q=... from the query string
    return f"Searching for: {query}"

@app.route("/login", methods=["POST"])
def login():
    username = request.form.get("username")  # a field from a submitted form
    password = request.form.get("password")
    return f"Logging in {username}"
  • request.args — query string parameters (?key=value in the URL), for any method.
  • request.form — parsed fields from an HTML form submitted with Content-Type: application/x-www-form-urlencoded or multipart/form-data.
  • request.json — a parsed JSON body, when the client sent Content-Type: application/json.

Returning JSON

For an API endpoint, return JSON with jsonify, which serializes Python data structures and sets the Content-Type: application/json response header for you:

Python
from flask import jsonify

@app.route("/api/users/<int:user_id>")
def api_user(user_id):
    return jsonify({"id": user_id, "name": "Ada Lovelace"})

Combining a path parameter, a restricted method, and a JSON response is the shape of most real API endpoints:

Python
@app.route("/api/users/<int:user_id>", methods=["GET"])
def get_user(user_id):
    users = {1: "Ada", 2: "Grace"}
    name = users.get(user_id)
    if name is None:
        return jsonify({"error": "User not found"}), 404
    return jsonify({"id": user_id, "name": name})

Returning a tuple of (response_body, status_code), as above, is how Flask views set a non-200 status code. Since Flask 1.1, returning a plain dict directly (without calling jsonify) is also automatically converted to a JSON response — but calling jsonify explicitly stays common because it's needed the moment you also want to set a custom status code or headers.

Common mistakes

  • Confusing request.args (query string) with request.form (submitted form body) — using the wrong one silently returns None instead of raising an error, which makes the bug easy to miss.
  • Assuming a route like /users/<user_id> gives you an int — without the int: converter, user_id arrives as a plain string, and comparisons like user_id == 1 will always be False.
  • Building a JSON API without ever setting explicit status codes, so every response — including errors — comes back as 200 OK, which breaks client-side error handling.

Interview questions

Q: What's the difference between request.args and request.form? request.args holds query string parameters parsed from the URL itself (?key=value), available regardless of HTTP method. request.form holds fields parsed from a request body sent as application/x-www-form-urlencoded or multipart/form-data — typically an HTML form submitted with POST. A JSON request body uses neither; it's read from request.json instead.

Q: How does Flask decide which converter to use for a route like /posts/<int:post_id>? The part before the colon inside <...> names the converter (int, float, path, uuid, or nothing for the default string matcher). Flask validates the URL segment against that converter while matching routes — if the segment doesn't satisfy it (e.g., non-digits against int), that route is treated as not matching at all, and Flask falls through to other routes or a 404.