Map URLs to views. Include app URLConfs, capture parts of the path with converters, name your routes, and build URLs in reverse instead of hard-coding them.
Why: the project urls.py is the front door — keep it thin and delegate each app’s routes with include(). Where: config/urls.py. This sends anything starting with blog/ to the blog app’s own URLConf.
# config/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls")), # delegate to the app
path("", include("blog.urls")), # also serve at the site root
]Why: each path() pairs a URL pattern with the view that answers it. Where: create blog/urls.py. app_name sets a namespace so route names never collide between apps — you refer to them as "blog:post_list".
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.post_list, name="post_list"),
path("new/", views.post_create, name="post_create"),
path("<slug:slug>/", views.post_detail, name="post_detail"),
]Why: the <type:name> syntax captures a piece of the URL and passes it to the view as an argument. When <int>: numeric IDs. When <slug>: URL-safe labels like my-first-post. When <str>: any text without a slash. <uuid> and <path> also exist.
path("posts/<int:pk>/", views.detail) # /posts/42/ → pk=42
path("<slug:slug>/", views.detail) # /my-post/ → slug="my-post"
path("authors/<str:username>/", views.author) # /authors/ada/
path("files/<path:filepath>/", views.serve) # captures slashes tooWhy: never hard-code "/blog/hello/" — if the URL pattern changes, every link breaks. Instead refer to the route by name and let Django build the path. When reverse(): in Python. When redirect(): to send the browser somewhere after a POST.
from django.urls import reverse
from django.shortcuts import redirect
url = reverse("blog:post_detail", kwargs={"slug": "hello"})
# → "/hello/"
def post_create(request):
post = Post.objects.create(...)
return redirect("blog:post_detail", slug=post.slug)Why: the template equivalent of reverse() — it keeps your HTML links in sync with the URLConf automatically. Note: pass arguments after the route name; the namespace ("blog:") matches the app_name you set.
<a href="{% url 'blog:post_list' %}">All posts</a>
{% for post in posts %}
<a href="{% url 'blog:post_detail' slug=post.slug %}">
{{ post.title }}
</a>
{% endfor %}When re_path: a pattern path converters cannot express — a four-digit year, a custom format. Note: named groups (?P<name>...) become the view’s keyword arguments, exactly like converters. Reach for path() first; use re_path only when you must.
from django.urls import re_path
urlpatterns = [
# /archive/2026/ → view(year="2026")
re_path(r"^archive/(?P<year>[0-9]{4})/$", views.archive),
]