Skip to main content

🛣️ Routing and View Functions

Routing is how Flask answers the question "which code runs for this URL?" In this lesson you'll wire URLs to Python functions with decorators, capture dynamic parts of a URL with converters, respond to different HTTP methods, return every kind of response, and build URLs the safe way with url_for().

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what routing is and connect URLs to view functions with @app.route()
  • Capture dynamic URL segments and validate them with type converters
  • Handle multiple HTTP methods (GET, POST, and REST verbs) in a single route
  • Return the full range of response types — strings, HTML, JSON, redirects, and custom status codes
  • Build URLs with url_for() and organize routes with Blueprints

Estimated Time: 35–45 minutes  •  Difficulty: Intermediate

Hands-on: Build the routing skeleton of a small blog, including a JSON API endpoint and a search route.

In This Lesson

What Is Routing?

Routing is the mechanism that maps URL patterns to the code that handles them. When a request arrives for a particular URL, the router decides which function in your application should process it and produce a response. That function is called a view function (or simply a "view").

graph LR A[Client Browser] -->|Request /products| B[Web Server] B --> C[Flask Router] C -->|Pattern matches /products| D[product_list view] D --> E[Build Response] E -->|HTML / JSON| A
💡 An analogy: Routing is the mailroom of a large office. A letter arrives (the request), the sorter reads the address (the URL), and delivers it to the right department (the view function). Get the addressing scheme right and everything lands where it should; get it wrong and mail piles up in the dead-letter bin (a 404).

Flask's Routing System

Flask connects URLs to functions with the @app.route() decorator. The string you pass is the URL rule; the function beneath it is the view that runs when the rule matches.

from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "Welcome to the Home Page!"

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

With the dev server running, visiting http://localhost:5000/ executes home(), and http://localhost:5000/about executes about(). The function's name also becomes its endpoint — the identifier url_for() uses later to build a link back to it.

📖 Key Terms

Rule: the URL pattern, e.g. /about or /user/<name>.

View function: the Python function that runs when a rule matches and returns a response.

Endpoint: the name Flask stores the rule under (defaults to the view function's name).

URL Parameters & Converters

Real apps need URLs like /user/john or /post/42. Flask captures a dynamic segment with angle brackets and passes it to the view as an argument:

@app.route("/user/<username>")
def show_user_profile(username):
    return f"User profile: {username}"

Visiting /user/john calls show_user_profile("john"). By default a captured value is a string. Adding a converter both validates the segment and casts it to the right type:

@app.route("/post/<int:post_id>")
def show_post(post_id):
    # post_id arrives already an int
    return f"Post #{post_id}"

@app.route("/files/<path:subpath>")
def show_file(subpath):
    # path converter matches slashes too
    return f"File at: {subpath}"
ConverterMatchesExample rule
string (default)Any text without a slash/user/<string:name>
intNon-negative integers/post/<int:id>
floatNon-negative decimal numbers/price/<float:amount>
pathLike string, but allows slashes/files/<path:filename>
uuidUUID strings/doc/<uuid:id>

✅ Converters give you free validation

Because <int:post_id> only matches digits, /post/banana never reaches your view — Flask returns a 404 automatically. You get input validation and a correctly typed argument for free, with zero code in the function body.

💡 Path parameters vs query strings

Use a path parameter (/post/42) to identify which resource. Use a query string (/products?sort=price&page=2) for optional filtering, sorting, and pagination. Read the query string with request.args.get("sort").

HTTP Methods

By default a route answers only GET requests. To handle others, list them in the methods argument. A classic pattern is one route that both shows a form (GET) and processes it (POST):

from flask import request

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]
        # ...authenticate the user...
        return f"Logging in {username}..."
    # GET: show the form
    return """
        <form method="post">
            <input type="text" name="username">
            <input type="password" name="password">
            <input type="submit" value="Login">
        </form>
    """
flowchart TD A[Client] -->|GET /login| B[login view] B --> C{request.method?} C -->|GET| D[Return login form] A -->|POST /login + form data| B C -->|POST| E[Process credentials] E --> F[Return result / redirect] D --> A F --> A

📖 REST verbs at a glance

MethodIntent
GETRead a resource (safe, no side effects)
POSTCreate a new resource
PUTReplace a resource entirely
PATCHPartially update a resource
DELETERemove a resource

Flask makes implementing these RESTful conventions as simple as listing the verb in methods and branching on request.method.

Anatomy of a View Function

A view function is where your application's logic lives for a given URL. Most views follow the same four-step shape: get data, handle edge cases, do any processing, and return a response.

from flask import render_template, abort

@app.route("/product/<int:product_id>")
def product_detail(product_id):
    # 1. Acquire data (e.g. from a database)
    product = get_product_from_database(product_id)

    # 2. Handle the not-found case
    if product is None:
        abort(404)

    # 3. Process / enrich the data
    similar = find_similar_products(product)

    # 4. Return a response (here, a rendered template)
    return render_template(
        "product_detail.html",
        product=product,
        similar_products=similar,
    )

Keeping this structure consistent makes views easy to read and test: each one acquires data, guards against bad input, prepares what the template needs, and hands back a response.

Response Types

A Flask view can return far more than a string. Here are the common shapes a response can take.

Plain string and HTML

@app.route("/hello")
def hello():
    return "Hello, World!"          # becomes a 200 text/html response

Rendered template

@app.route("/dashboard")
def dashboard():
    user = get_current_user()
    stats = get_user_stats(user.id)
    return render_template("dashboard.html", user=user, stats=stats)

JSON

from flask import jsonify

@app.route("/api/products")
def product_list_api():
    products = get_all_products()
    return jsonify(
        products=[p.to_dict() for p in products],
        count=len(products),
    )

💡 Return a dict directly

In modern Flask (1.1+), returning a dict or list from a view is automatically converted to a JSON response — return {"status": "ok"} works without calling jsonify. jsonify is still handy when you want to set a status code or headers.

Redirects

from flask import redirect, url_for

@app.route("/old-page")
def old_page():
    return redirect(url_for("new_page"))

@app.route("/new-page")
def new_page():
    return "This is the new page"

Custom status codes

@app.route("/created", methods=["POST"])
def created_example():
    return {"message": "Resource created"}, 201     # (body, status_code)

@app.route("/missing")
def missing_example():
    return "Not found", 404

Returning a tuple of (body, status_code) — or (body, status_code, headers) — lets you control the exact HTTP response without building a full Response object.

URL Building with url_for()

Never hard-code URLs in your templates or redirects. Instead, ask Flask to build them from the endpoint name with url_for():

from flask import url_for

@app.route("/")
def index():
    login_url = url_for("login")                       # -> /login
    profile_url = url_for("profile", username="john")  # -> /user/john
    return f'<a href="{login_url}">Login</a> <a href="{profile_url}">John</a>'

@app.route("/login")
def login():
    return "Login Page"

@app.route("/user/<username>")
def profile(username):
    return f"Profile page for {username}"

✅ Why url_for() beats hard-coded strings

  • If you change a URL rule, every link updates automatically — no broken paths.
  • It fills in dynamic segments and correctly URL-encodes special characters.
  • It respects an application mounted under a sub-path (a SCRIPT_NAME prefix).
  • Extra keyword arguments that aren't route parameters become query-string values.

In Jinja2 templates you use the very same function: <a href="{{ url_for('profile', username=user.name) }}">.

Organizing with Blueprints

As an app grows, keeping every route in one file becomes unmanageable. Blueprints let you split routes into self-contained modules and register them under a common URL prefix.

# auth.py
from flask import Blueprint

auth_bp = Blueprint("auth", __name__, url_prefix="/auth")

@auth_bp.route("/login")
def login():
    return "Login page"

@auth_bp.route("/register")
def register():
    return "Registration page"
# app.py
from flask import Flask
from auth import auth_bp

app = Flask(__name__)
app.register_blueprint(auth_bp)

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

Because the blueprint sets url_prefix="/auth", its login view lives at /auth/login. Its endpoint becomes auth.login, so you link to it with url_for("auth.login").

graph TD A[Flask Application] --> B[Main routes] A --> C[auth blueprint /auth] A --> D[admin blueprint /admin] A --> E[api blueprint /api] C --> C1[/auth/login/] C --> C2[/auth/register/] D --> D1[/admin/dashboard/] E --> E1[/api/products/]

💡 Blueprints + application factory

The professional Flask layout pairs Blueprints with an application factory — a create_app() function that builds the app and registers each blueprint. This keeps features isolated, makes testing easier, and avoids circular imports. You'll build one later in the module.

Hands-on Exercise

🏋️ Build a Blog's Routing Skeleton

Objective: Create the routes and view functions for a small blog using in-memory dummy data (no database yet). Focus purely on the routing structure.

Requirements — create routes for:

  1. / — homepage listing all posts
  2. /post/<int:post_id> — a single post detail page (404 if it doesn't exist)
  3. /category/<name> — posts filtered by category
  4. /api/posts — all posts as JSON
  5. /search — reads a ?q= query parameter and returns matching titles
💡 Hint

Store posts as a list of dicts. Use an <int:post_id> converter for the detail route and abort(404) when no post matches. Read the search term with request.args.get("q", ""). Return a dict or jsonify(...) for the API route.

✅ Solution
from flask import Flask, request, abort, jsonify

app = Flask(__name__)

POSTS = [
    {"id": 1, "title": "Hello Flask", "category": "python", "body": "First post."},
    {"id": 2, "title": "Routing Deep Dive", "category": "python", "body": "URLs to views."},
    {"id": 3, "title": "Design Notes", "category": "design", "body": "On layout."},
]

def find_post(post_id):
    return next((p for p in POSTS if p["id"] == post_id), None)

@app.route("/")
def home():
    titles = ", ".join(p["title"] for p in POSTS)
    return f"<h1>Latest posts</h1><p>{titles}</p>"

@app.route("/post/<int:post_id>")
def post_detail(post_id):
    post = find_post(post_id)
    if post is None:
        abort(404)
    return f"<h1>{post['title']}</h1><p>{post['body']}</p>"

@app.route("/category/<name>")
def category(name):
    matches = [p["title"] for p in POSTS if p["category"] == name]
    return {"category": name, "posts": matches}

@app.route("/api/posts")
def api_posts():
    return jsonify(posts=POSTS, count=len(POSTS))

@app.route("/search")
def search():
    q = request.args.get("q", "").lower()
    hits = [p["title"] for p in POSTS if q in p["title"].lower()]
    return {"query": q, "results": hits}

if __name__ == "__main__":
    app.run(debug=True)

Try /, /post/2, /post/99 (404), /category/python, /api/posts, and /search?q=routing. Bonus: move the API route into a Blueprint registered under /api.

🎯 Quick Quiz

Question 1: Given @app.route("/post/<int:post_id>"), what happens when a user visits /post/banana?

Question 2: Why is url_for("profile", username="john") preferred over hard-coding "/user/john"?

Question 3: What is the main purpose of a Blueprint?

Summary & Quiz

🎉 Key Takeaways

  • Routing maps URL rules to view functions via the @app.route() decorator.
  • Dynamic segments in angle brackets become view arguments; converters like int validate and cast them.
  • List HTTP methods in methods=[...] and branch on request.method to follow REST conventions.
  • Views can return strings, HTML, templates, JSON (or a plain dict), redirects, and (body, status) tuples.
  • Build URLs with url_for(), and split large apps into Blueprints.

📚 Further Reading

🚀 What's Next?

Your views can now return rendered templates — but we've been writing HTML as raw strings. Next you'll learn the Jinja2 template system: variables, control structures, filters, inheritance, and macros for producing clean, maintainable HTML.

🎉 Great work!

You can now route any URL to the right code and return any kind of response. Let's make that HTML beautiful with templates.