Routing Basics

Every page Bookshelf will ever serve — the home page, the book list, a single shelf's contents — starts with the same idea: a URL path mapped to a Python function that decides what to send back. This lesson covers that mapping in depth: registering routes, restricting which HTTP methods they accept, and generating URLs the right way instead of typing paths by hand.

The @app.route Decorator

@app.route() is how a URL path gets connected to a Python function, called a view function. Whatever that function returns becomes the HTTP response. Here are several of Bookshelf's earliest routes, each with its own view function:

Example: Bookshelf's First Routes

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def home():
    return render_template('home.html')

@app.route('/books')
def book_list():
    return render_template('books/list.html')

@app.route('/shelves')
def shelf_list():
    return render_template('shelves/list.html')

@app.route('/about')
def about():
    return 'Bookshelf is a personal reading tracker.'

home, book_list, shelf_list, and about are ordinary Python functions — the decorator is what makes Flask aware of them and connects each one to a specific path. render_template() (covered fully in Jinja2 Templates) renders a Jinja2 template file into HTML; a view can just as easily return a plain string, as about() does here.

Restricting Which HTTP Methods a Route Accepts

By default, a route only responds to GET requests — a plain page load. Some routes, like a form for adding a new book to Bookshelf, need to accept a submitted form too, which arrives as a POST request. The methods argument lists exactly which HTTP methods a route allows:

@app.route('/books/new', methods=['GET', 'POST'])
def add_book():
    return 'Form goes here'

Requesting /books/new with any method not listed — a DELETE request, for instance — gets a 405 Method Not Allowed response automatically, with no extra code required. Handling GET and POST differently inside a single view function — showing an empty form on GET, processing submitted data on POST — is covered in full in the next lesson, Request & Response Objects.

Why Hardcoded URLs Are Fragile

It's tempting to just type /books directly, everywhere Bookshelf needs to link to the book list — in navigation menus, in redirects, in every template that mentions it. The problem shows up the moment that path ever changes: renaming /books to /library means hunting down and fixing every hardcoded reference by hand, and it's easy to miss one.

Generating URLs with url_for()

Flask's url_for() function solves this by generating a URL from a view function's name rather than its path string:

from flask import url_for

url_for('book_list')  # '/books'

Because url_for('book_list') asks Flask to look up whatever path is currently registered to the book_list view, changing the route itself is now enough:

@app.route('/library')
def book_list():
    return render_template('books/list.html')

Every url_for('book_list') call anywhere in the project — templates, redirects, other view functions — now automatically resolves to /library, with zero additional edits required. That single change is exactly what url_for() buys you.

Using url_for() Inside Templates

The most common place url_for() shows up is directly inside Jinja2 templates, for every internal link:

<nav>
  <a href="{{ url_for('home') }}">Bookshelf</a>
  <a href="{{ url_for('book_list') }}">All Books</a>
  <a href="{{ url_for('shelf_list') }}">Shelves</a>
  <a href="{{ url_for('about') }}">About</a>
</nav>

Every link in that navigation bar stays correct automatically no matter how the underlying paths change, since none of them are typed as literal strings. This pattern comes up constantly once Template Inheritance & Includes puts a shared navigation bar into a base template used by every page on the site.

Flask's "Did You Mean" 404 Behavior

One small but genuinely pleasant built-in touch: during development, if you request a URL that doesn't exist but is close to one that does — say, /boks instead of /books — Werkzeug (the toolkit Flask is built on) will sometimes include a "did you mean" suggestion pointing at the closest registered route, right alongside the normal 404 response. It's not a substitute for proper error pages (covered in Error Handling), but it's a handy nudge while you're still actively typing out routes by hand during development.

Trailing Slashes: /books vs /books/

Flask treats a route defined without a trailing slash, like /books, differently from one defined with one, like /books/. If you register /books and a visitor requests /books/, Flask responds with a 404 — that exact trailing-slash variant was never registered. Flip it around, though, and Flask is more forgiving: if you register /books/ (with the slash) and a visitor requests /books (without it), Flask automatically redirects to the version with the slash.

Note: This asymmetry is a small but real detail worth being consistent about. Pick one style for Bookshelf's routes — most projects prefer no trailing slash for pages like /books and /shelves — and stick with it, so visitors and search engines aren't quietly redirected back and forth between two versions of what's really the same page.
Next up: Fixed routes like /books only get you so far — head to Dynamic Routes & Converters to see how a single route handles a specific book, like /books/42.