π§ Routing and View Functions
Routing is how a URL finds the code that answers it. Flask's decorator-based routing is one of its most elegant features β clean, declarative, and Pythonic. This lesson takes you from a plain / route all the way to dynamic URLs, converters, url_for, rich responses, and blueprints for organizing a growing app.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Map URLs to view functions with the
@app.routedecorator - Handle multiple HTTP methods (GET, POST, and friends) on one endpoint
- Capture dynamic URL variables using built-in and custom converters
- Generate URLs safely with url_for instead of hardcoding them
- Return the full range of responses β text, HTML, JSON, redirects, and status codes
- Group routes with blueprints for larger applications
Estimated Time: 40β50 minutes β’ Difficulty: Intermediate
Hands-on: Build a small URL-shortener that exercises dynamic routes, redirects, and error handling.
In This Lesson
What Is Routing?
Routing is the process of deciding which piece of your code answers a given URL and HTTP method. In Flask you declare routes with the @app.route decorator, which pairs a URL pattern with a Python function called a view function. When a request arrives, Flask's URL dispatcher finds the matching route and runs its view.
GET /about] --> B[Flask URL
dispatcher] B --> C{Pattern
matches?} C -->|Yes| D[Run view
function] C -->|No| E[404 Not Found] D --> F[Response] E --> F F --> A
Think of routing as a receptionist directing visitors: the URL is the name a visitor asks for, and the router walks them to the right office (view function). If nobody by that name exists, they're politely turned away with a 404.
Basic Routes & HTTP Methods
The simplest route maps a fixed path to a function. Whatever the function returns becomes the response body.
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Hello, World!"
@app.route("/about")
def about():
return "About page"
By default a route answers only GET requests. To accept others, list them in methods. This is how one URL can both show a form (GET) and process it (POST):
from flask import request, render_template
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form["username"]
return f"Logging in {username}..."
# GET: show the form
return render_template("login.html")
π The common HTTP methods
GET β retrieve data (must not change anything). POST β submit new data. PUT β replace an existing resource. PATCH β partially update it. DELETE β remove it. RESTful APIs lean on all five.
Dynamic Routes & Converters
Real apps need URLs like /user/ray or /post/42. Flask captures the changing part with angle brackets and passes it to your view as an argument.
@app.route("/user/<username>")
def show_user(username):
return f"User: {username}"
@app.route("/post/<int:post_id>")
def show_post(post_id):
return f"Post number {post_id}"
The <int:post_id> syntax uses a converter: it only matches integers and hands your function a real int, not a string. Flask ships several converters:
| Converter | Matches | Example |
|---|---|---|
string (default) | Any text without a slash | /user/<name> |
int | Positive integers | /post/<int:id> |
float | Positive decimals | /price/<float:amt> |
path | Text including slashes | /files/<path:sub> |
uuid | UUID strings | /item/<uuid:id> |
You can combine several variables in one pattern:
@app.route("/blog/<int:year>/<int:month>/<int:day>")
def blog_by_date(year, month, day):
return f"Posts from {year}-{month:02d}-{day:02d}"
When the built-ins aren't enough, write a custom converter. Here's one that splits a plus-separated list, so /tags/python+flask+web arrives as a Python list:
from werkzeug.routing import BaseConverter
class ListConverter(BaseConverter):
def to_python(self, value):
return value.split("+")
def to_url(self, values):
return "+".join(str(v) for v in values)
app.url_map.converters["list"] = ListConverter
@app.route("/tags/<list:tags>")
def show_tags(tags):
return f"Tags: {', '.join(tags)}"
π‘ Converters validate for you
Because <int:post_id> only matches digits, a request to /post/banana never even reaches your view β Flask returns a 404 automatically. Converters are your first, free layer of input validation.
Building URLs with url_for
Never hardcode URLs. Instead, ask Flask to build them from the view function's name with url_for(). If you later change a route's path, every generated URL updates automatically β no broken links.
from flask import url_for, redirect
@app.route("/user/<username>")
def profile(username):
return f"Profile: {username}"
@app.route("/me")
def me():
# Build the URL for profile(username="ray")
return redirect(url_for("profile", username="ray"))
url_for handles more than the path. Extra keyword arguments become query-string parameters, and special ones control the output:
url_for("search", q="flask tutorial", page=2)
# -> "/search?q=flask+tutorial&page=2"
url_for("home", _external=True)
# -> "http://example.com/"
url_for("profile", username="ray", _anchor="bio")
# -> "/user/ray#bio"
β Always use url_for in templates and redirects
In Jinja2 you'll write <a href="{{ url_for('about') }}">About</a> rather than <a href="/about">. It keeps links correct through refactors, escapes parameters properly, and respects blueprint prefixes.
View Functions & Responses
A view function reads the request and returns a response. The request object holds everything about the incoming call:
| Attribute | Contains |
|---|---|
request.method | The HTTP method (GET, POSTβ¦) |
request.args | Query-string parameters (?q=flask) |
request.form | Submitted form fields |
request.json | Parsed JSON body |
request.files | Uploaded files |
request.headers | Request headers |
View functions can return many kinds of response. Flask is flexible about what "a response" is:
from flask import jsonify, render_template, make_response, redirect, url_for, abort
@app.route("/text")
def text():
return "Plain text" # a string becomes an HTML response
@app.route("/page")
def page():
return render_template("page.html", title="Hi") # rendered template
@app.route("/api/data")
def data():
return jsonify(name="Flask", awesome=True) # JSON response
@app.route("/old")
def old():
return redirect(url_for("page")) # 302 redirect
@app.route("/missing")
def missing():
abort(404) # raise an HTTP error
@app.route("/custom")
def custom():
resp = make_response("Created!", 201) # body + status code
resp.headers["X-Custom"] = "value"
return resp
You can attach a status code simply by returning a tuple β return body, status. Here's a compact JSON API using that idiom:
from flask import Flask, request, jsonify
app = Flask(__name__)
USERS = {"1": {"id": "1", "name": "John"}}
@app.route("/api/users/<user_id>")
def get_user(user_id):
user = USERS.get(user_id)
if user is None:
return jsonify(error="User not found"), 404
return jsonify(user)
@app.route("/api/users", methods=["POST"])
def create_user():
if not request.is_json:
return jsonify(error="JSON required"), 415
data = request.get_json()
if "name" not in data:
return jsonify(error="'name' is required"), 400
new_id = str(len(USERS) + 1)
USERS[new_id] = {"id": new_id, "name": data["name"]}
return jsonify(USERS[new_id]), 201
Organizing with Blueprints
As routes multiply, a single file gets unwieldy. Blueprints let you group related routes β authentication, the main site, an admin area β into separate modules that you register onto the app.
# app/auth/routes.py
from flask import Blueprint, render_template, redirect, url_for
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
return render_template("auth/login.html")
@auth_bp.route("/logout")
def logout():
return redirect(url_for("main.home"))
# app/__init__.py
def create_app():
app = Flask(__name__)
from app.auth.routes import auth_bp
from app.main.routes import main_bp
app.register_blueprint(auth_bp)
app.register_blueprint(main_bp)
return app
The url_prefix="/auth" means every route in that blueprint is served under /auth/.... When building URLs to a blueprint route, prefix the endpoint with the blueprint name:
url_for("auth.login") # -> "/auth/login"
url_for("main.home") # -> "/"
Error Handling
When something goes wrong you want a friendly, consistent response β an HTML page for a website, JSON for an API. Register error handlers for the status codes you care about:
@app.errorhandler(404)
def not_found(error):
return render_template("errors/404.html"), 404
@app.errorhandler(500)
def server_error(error):
return render_template("errors/500.html"), 500
Use abort() to trigger an error from inside a view, and combine it with checks for a clean guard-clause style:
from flask import abort
@app.route("/user/<username>")
def show_user(username):
user = lookup_user(username)
if user is None:
abort(404) # not found
if not current_user_can_view(user):
abort(403) # forbidden
return render_template("user.html", user=user)
β οΈ Match the error format to the client
An API should return JSON errors, not an HTML page a JavaScript client can't parse. A common pattern is checking request.path.startswith("/api") (or the Accept header) in the handler and returning jsonify(...) for API routes.
Hands-on Exercise
ποΈ Build a Tiny URL Shortener
Objective: Exercise dynamic routes, redirects, form handling, and error handling in one small app.
Instructions:
- Create a route
/(GET) that shows a form where the user pastes a long URL. - Create a route
/shorten(POST) that reads the URL fromrequest.form, generates a short code, and stores the mapping in a dictionary. - Create a dynamic route
/<code>that looks up the code and redirects to the original URL. - If the code doesn't exist, return a 404 with a friendly message.
- Bonus: add a
/<code>/statsroute that reports how many times the short link has been visited.
π‘ Hint
Generate a short code with secrets.token_urlsafe(4). Store both the URL and a visit counter, e.g. LINKS[code] = {"url": long_url, "visits": 0}. Use redirect() for the lookup route and abort(404) when the code is missing.
β Sample solution
import secrets
from flask import Flask, request, redirect, abort, render_template_string
app = Flask(__name__)
LINKS = {}
FORM = """
<form method="post" action="/shorten">
<input name="url" placeholder="https://..." size="40">
<button>Shorten</button>
</form>
"""
@app.route("/")
def index():
return render_template_string(FORM)
@app.route("/shorten", methods=["POST"])
def shorten():
url = request.form["url"]
code = secrets.token_urlsafe(4)
LINKS[code] = {"url": url, "visits": 0}
return f"Short link: /{code}"
@app.route("/<code>")
def follow(code):
link = LINKS.get(code)
if link is None:
abort(404)
link["visits"] += 1
return redirect(link["url"])
@app.route("/<code>/stats")
def stats(code):
link = LINKS.get(code)
if link is None:
abort(404)
return f"{code} -> {link['url']} ({link['visits']} visits)"
@app.errorhandler(404)
def not_found(e):
return "That short link doesn't exist.", 404
if __name__ == "__main__":
app.run(debug=True)
π― Quick Quiz
Question 1: What does <int:post_id> in a route do?
Question 2: Why use url_for("about") instead of writing "/about"?
Question 3: When building a URL to a route inside a blueprint named auth, what do you pass to url_for?
Summary & Quiz
π Key Takeaways
- Routes map URLs to view functions via the
@app.routedecorator; the return value is the response. - List
methods=[...]to handle POST and other verbs on one endpoint. - Dynamic routes capture URL parts; converters like
intvalidate and type them for free. - Always build URLs with url_for β never hardcode paths.
- Views can return text, HTML, JSON, redirects, and custom responses with status codes.
- Blueprints organize routes into modules; error handlers keep failures consistent.
π Further Reading
- Flask β Routing
- Flask β Modular Applications with Blueprints
- Flask β Handling Application Errors
- Werkzeug β URL Routing & Converters
π What's Next?
Your routes currently return bare strings and hand-built HTML. Next we bring in the Jinja2 templating engine so you can render clean, reusable HTML pages with layouts, loops, and dynamic data.
π Well routed!
You can direct any request to the right code and return exactly the response it needs. On to templates.