Skip to main content

🧭 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.route decorator
  • 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.

flowchart LR A[Request
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:

ConverterMatchesExample
string (default)Any text without a slash/user/<name>
intPositive integers/post/<int:id>
floatPositive decimals/price/<float:amt>
pathText including slashes/files/<path:sub>
uuidUUID 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:

AttributeContains
request.methodThe HTTP method (GET, POST…)
request.argsQuery-string parameters (?q=flask)
request.formSubmitted form fields
request.jsonParsed JSON body
request.filesUploaded files
request.headersRequest 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")    # -> "/"
graph TB A[Flask app] --> B["auth blueprint<br/>/auth prefix"] A --> C["main blueprint<br/>/ prefix"] A --> D["admin blueprint<br/>/admin prefix"] B --> E["/auth/login"] B --> F["/auth/logout"] C --> G["/ (home)"] C --> H["/about"] D --> I["/admin/users"]

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:

  1. Create a route / (GET) that shows a form where the user pastes a long URL.
  2. Create a route /shorten (POST) that reads the URL from request.form, generates a short code, and stores the mapping in a dictionary.
  3. Create a dynamic route /<code> that looks up the code and redirects to the original URL.
  4. If the code doesn't exist, return a 404 with a friendly message.
  5. Bonus: add a /<code>/stats route 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.route decorator; the return value is the response.
  • List methods=[...] to handle POST and other verbs on one endpoint.
  • Dynamic routes capture URL parts; converters like int validate 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

πŸš€ 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.