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):
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:
@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:
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.
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=valuein the URL), for any method.request.form— parsed fields from an HTML form submitted withContent-Type: application/x-www-form-urlencodedormultipart/form-data.request.json— a parsed JSON body, when the client sentContent-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:
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:
@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) withrequest.form(submitted form body) — using the wrong one silently returnsNoneinstead of raising an error, which makes the bug easy to miss. - Assuming a route like
/users/<user_id>gives you anint— without theint:converter,user_idarrives as a plain string, and comparisons likeuser_id == 1will always beFalse. - 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.