URLs and Routing
Introduction
Django maps incoming URL paths to Python callables through URLconf modules—Python lists of path() (or re_path()) entries. This chapter builds on the blog/urls.py wiring from chapter 03: path converters, include(), named URLs, reverse(), and RESTful routing conventions that reappear in class-based views and Django REST Framework.
Prerequisites
- Your First Django Project —
blogapp with roothomeview - Project Structure — know
config/urls.pyvsblog/urls.py
URLconf Basics
Root vs app URLconfs
config/urls.py (project root):
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("blog.urls")),
path("blog/", include("blog.urls")), # optional prefix — pick one pattern
]Tip
One Mount Point
Use either path("", include(...)) or path("blog/", include(...)), not both with duplicate patterns, unless you intentionally expose the same app at two prefixes.
blog/urls.py (app):
from django.urls import path
from . import views
urlpatterns = [
path("", views.home, name="home"),
path("posts/", views.post_list, name="post-list"),
path("posts/<int:pk>/", views.post_detail, name="post-detail"),
]If mounted at path("blog/", include("blog.urls")), the list URL is /blog/posts/.
path() signature
path(route, view, kwargs=None, name=None)| Part | Meaning |
|---|---|
route | URL pattern string (no leading domain) |
view | Callable or View.as_view() |
name | Logical name for reverse lookup |
kwargs | Extra dict passed to view (rare at path level) |
Path Converters
Capture part of the URL and pass it as a view argument:
| Converter | Matches | View receives |
|---|---|---|
str | Non-empty string (default) | str |
int | Integer | int |
slug | Slug ([-a-zA-Z0-9_]+) | str |
uuid | UUID | UUID |
path | Any path including / | str |
Examples:
path("posts/<int:pk>/", views.post_detail),
path("category/<slug:slug>/", views.category),
path("files/<path:filepath>/", views.serve_file),blog/views.py (stubs for routing practice):
Code explanation:
pkin the path becomespost_detail(request, pk=1)for/posts/1/- Converter name in angle brackets must match the view parameter name
include() for App Routes
path("api/", include("blog.urls")),
path("accounts/", include("django.contrib.auth.urls")),Benefits:
- Each app owns its
urls.py - Reusable apps ship URLconf you mount once
- Root file stays small
Namespaces (optional)
When the same app is included twice or you need disambiguation:
# config/urls.py
path("blog/", include(("blog.urls", "blog"), namespace="blog")),Template: {% url 'blog:post-detail' pk=1 %}. Use when docs or third-party apps require it; simple projects often skip namespaces.
re_path() (Regular Expressions)
Legacy and edge cases:
from django.urls import re_path
re_path(r"^posts/(?P<year>[0-9]{4})/$", views.posts_by_year),Prefer path() with converters for readability. Use re_path() when patterns do not fit converters.
Named URLs and Reverse Resolution
name= in path()
path("posts/<int:pk>/", views.post_detail, name="post-detail"),reverse() in Python
from django.shortcuts import redirect
from django.urls import reverse
def some_view(request):
url = reverse("post-detail", args=[42])
# '/posts/42/' or '/blog/posts/42/' depending on mount
return redirect("post-detail", pk=42) # shortcutredirect() shortcuts
return redirect("home")
return redirect("post-detail", pk=1)
return redirect("/hard-coded/") # avoid when possibleHard-coded paths break when URL structure changes; named URLs stay stable.
Templates (preview)
In templates (covered in chapter 08):
<a href="{% url 'post-detail' pk=post.id %}">Read more</a>Requires name="post-detail" on the path.
Query Strings vs Path Parameters
Path: /posts/5/ → view arg pk=5
Query: /posts/?page=2&tag=django → request.GET
def post_list(request):
page = request.GET.get("page", "1")
tag = request.GET.get("tag")
...Use path segments for resource identity; query strings for filters, pagination, optional params.
RESTful Routing Conventions
Conceptual map (full CRUD in views chapter):
| Method | Path | Action |
|---|---|---|
| GET | /posts/ | List |
| GET | /posts/<id>/ | Detail |
| POST | /posts/ | Create |
| PUT/PATCH | /posts/<id>/ | Update |
| DELETE | /posts/<id>/ | Delete |
Django URLconf only maps path + view; HTTP method dispatch happens in the view (@require_http_methods, CBV, or DRF). Same path, different methods:
@require_http_methods(["GET", "POST"])
def post_list(request):
if request.method == "GET":
...
elif request.method == "POST":
...DRF ViewSets and routers automate this table—chapter 19.
Trailing Slashes
Django APPEND_SLASH (default True) redirects /posts → /posts/ when a trailing-slash pattern exists.
Convention: define patterns with trailing slash:
path("posts/", views.post_list), # preferredCompare: Spring MVC routing uses different path rules—Django’s slash behavior is easy to miss in API-only apps (APPEND_SLASH=False sometimes used for REST).
Debugging URL Issues
python manage.py show_urlsIf django-extensions is not installed, inspect manually or add temporary logging:
# config/urls.py — dev only
from django.urls import get_resolver
print(get_resolver().url_patterns)Common errors:
| Error | Cause |
|---|---|
| 404 | Pattern order wrong; more specific paths must come before catch-alls |
| NoReverseMatch | Wrong name, missing arg, or namespace |
| TypeError | View missing pk parameter matching converter |
Full Example: blog/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("", views.home, name="home"),
path("posts/", views.post_list, name="post-list"),
path("posts/new/", views.post_create, name="post-create"),
path("posts/<int:pk>/", views.post_detail, name="post-detail"),
path("posts/<int:pk>/edit/", views.post_edit, name="post-edit"),
path("category/<slug:slug>/", views.category, name="category"),
]config/urls.py:
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("blog.urls")),
]Test:
python manage.py runserver
curl http://127.0.0.1:8000/posts/1/Post-Chapter Checklist
- You can add
path()entries withintandslugconverters - You use
include("blog.urls")from the project root - You assign
name=and usereverse()/redirect() - You distinguish path args from
request.GET - You know RESTful path naming for later CRUD chapters
FAQ
path() vs url()?
url() was removed in Django 4+—use path() / re_path().
API without trailing slash?
Set APPEND_SLASH = False in settings and define patterns consistently; document for API clients.
Multiple parameters?
path("archive/<int:year>/<int:month>/", views.archive),View: def archive(request, year, month):
External link in {% url %}?
{% url %} is for internal routes only. Use plain href="https://...".
Same name in two apps?
Use namespaces or unique names like blog-post-detail vs shop-item-detail.