URL Routing
🧭 URL Routing
@app.get("/users/{id}") decorator sits directly above the function it triggers. Django keeps these separate on purpose: a dedicated urls.py file is the single table mapping every incoming path to the view that handles it. This chapter is about reading and writing that table.
A Separate Routing Table
FastAPI's Decorators vs Django's urls.py
FastAPI: Route Lives on the Function
To know every route in the app, you search for every @app.get/@app.post decorator across all your files.
Django: One Central Table
Every route for this app is visible in one file, independent of where get_book itself is defined.
path() and Converters
path() takes a route string, a view to call, and an optional name. Angle brackets capture URL segments and convert them to a specific Python type before your view ever sees them:
<int:book_id>
Matches digits only and converts to a Python int — the equivalent of FastAPI's book_id: int type hint, just expressed in the route string itself.
<str:slug>
Matches any non-empty string except a forward slash — the default converter if you omit a type (<slug> alone works too).
<uuid:token>
Matches a formatted UUID string and converts it to a Python UUID object automatically.
Path Parameter, Not Query String
Just like FastAPI, these are segments of the path itself — query strings (?page=2) are read separately from request.GET inside the view.
🔗 Wiring Apps Into the Project
Chapter 1 introduced project vs app. Routing is where that split becomes concrete: the project's root urls.py doesn't list every route directly — it include()s each app's own urls.py under a prefix:
Root urls.py Delegating to an App
Every pattern inside catalog/urls.py is now reachable under /books/ — path("<int:book_id>/", ...) inside that file resolves to /books/<int:book_id>/ overall. An app's own urls.py never needs to know or care what prefix it will eventually sit under.
re_path(): Regex Routes
For patterns path()'s converters can't express, re_path() accepts a full regular expression — the same syntax from the Regular Expressions course, just applied to URL matching instead of text search:
Named groups ((?P<year>...)) are passed to the view as keyword arguments, the same way path()'s converters are — re_path() is the escape hatch for the (now rare) cases where a plain path() pattern isn't expressive enough.
🏷️ Named URLs: Never Hardcode a Path
Every example above passed a name= argument — that name lets templates and Python code build a URL by name instead of typing the literal path, so renaming a route doesn't mean hunting down every hardcoded string:
reverse()
The Python function version — used in views, redirects, and tests wherever a URL needs to be built programmatically.
{% url %}
The template tag version — used in HTML templates so a link's target is never a brittle, hand-typed string.
💻 Coding Challenges
Challenge 1: Write an App's urls.py
For a catalog app, write urlpatterns with three routes: a book list at the app's root, a book detail using an int converter, and a category listing using a str (or slug) converter — each with a sensible name.
Goal: Practice path() converters and naming routes from the start rather than as an afterthought.
Challenge 2: Wire It Into the Project
Write the project's root urls.py that includes the catalog app's URLs under the /books/ prefix, plus a second app accounts included under /accounts/. State the final full path for each of the three routes from Challenge 1.
Goal: Practice the project-to-app URL delegation pattern and reasoning about the combined prefix + pattern.
Challenge 3: Use reverse() Instead of a Hardcoded Path
Given a view function that currently redirects with a hardcoded string like redirect("/books/42/"), rewrite it to use reverse("book-detail", kwargs={"book_id": 42}) instead, and explain in one sentence why this matters if the /books/ prefix ever changes.
Goal: Practice never hardcoding a URL that a named route already describes.
include() a New App's URLsWriting a perfectly correct catalog/urls.py does nothing on its own — Django only knows about it once the project's root urls.py actually include()s it. Visiting a route that "should" exist and getting a 404 is very often this exact mistake, not a typo in the app's own URL patterns. It's the routing-layer sibling of Chapter 1's INSTALLED_APPS gotcha: the file existing on disk and Django actually knowing about it are two separate steps.
🎯 What's Next
URL patterns now know which Python callable to invoke — the next chapter is about what that callable actually looks like: Views, comparing Django's function-based views against class-based views, and how the request/response objects differ from FastAPI's.