URL Routing

Django Fundamentals — URL Routing
Django Fundamentals
Course 1 · Chapter 2 · URL Routing

🧭 URL Routing

In FastAPI, a route and its handler are the same piece of code — the @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
@app.get("/books/{book_id}") def get_book(book_id: int): return {"id": book_id}

To know every route in the app, you search for every @app.get/@app.post decorator across all your files.

Django: One Central Table
# catalog/urls.py from django.urls import path from . import views urlpatterns = [ path("books/<int:book_id>/", views.get_book, name="book-detail"), ]

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:

from django.urls import path from . import views urlpatterns = [ path("", views.book_list, name="book-list"), path("<int:book_id>/", views.book_detail, name="book-detail"), path("category/<str:slug>/", views.by_category, name="book-category"), path("<uuid:token>/confirm/", views.confirm, name="book-confirm"), ]

<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

# mysite/urls.py — the PROJECT's root routing table from django.contrib import admin from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), path("books/", include("catalog.urls")), path("accounts/", include("accounts.urls")), ]

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:

from django.urls import re_path from . import views urlpatterns = [ re_path(r"^archive/(?P<year>[0-9]{4})/$", views.year_archive, name="year-archive"), ]

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:

# In a view (Python) from django.urls import reverse url = reverse("book-detail", kwargs={"book_id": 42}) # -> "/books/42/" — built from the URL pattern, not a hand-typed string <!-- In a template (Django Template Language, Chapter 4) --> <a href="{% url 'book-detail' book_id=42 %}">View Book</a>

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.

→ Solution

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.

→ Solution

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.

→ Solution

⚠️ Gotcha: Forgetting to include() a New App's URLs

Writing 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.