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.