Django's URL dispatcher is a powerful pattern-matching system that maps incoming URLs to view functions. It provides clean, readable URLs while maintaining flexibility for complex routing requirements.
# myproject/urls.py (Root URLconf)
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls')),
path('api/v1/', include('api.urls')),
path('accounts/', include('django.contrib.auth.urls')),
path('', include('core.urls')), # Root patterns
]
# Serve media files in development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# Custom error handlers
handler404 = 'core.views.custom_404'
handler500 = 'core.views.custom_500'
handler403 = 'core.views.custom_403'
handler400 = 'core.views.custom_400'
# blog/urls.py
from django.urls import path, re_path
from . import views
app_name = 'blog' # Namespace for URL reversing
urlpatterns = [
# Basic patterns
path('', views.post_list, name='post_list'),
path('create/', views.post_create, name='post_create'),
# Parameter patterns
path('<int:pk>/', views.post_detail, name='post_detail'),
path('<int:pk>/edit/', views.post_edit, name='post_edit'),
path('<int:pk>/delete/', views.post_delete, name='post_delete'),
# Slug patterns
path('<slug:slug>/', views.post_by_slug, name='post_by_slug'),
path('category/<slug:category_slug>/', views.posts_by_category, name='posts_by_category'),
# Date-based patterns
path('<int:year>/', views.posts_by_year, name='posts_by_year'),
path('<int:year>/<int:month>/', views.posts_by_month, name='posts_by_month'),
path('<int:year>/<int:month>/<int:day>/', views.posts_by_day, name='posts_by_day'),
# Complex patterns
path('author/<str:username>/', views.posts_by_author, name='posts_by_author'),
path('tag/<str:tag_name>/', views.posts_by_tag, name='posts_by_tag'),
# API endpoints
path('api/posts/', views.api_post_list, name='api_post_list'),
path('api/posts/<int:pk>/', views.api_post_detail, name='api_post_detail'),
]
Django provides built-in path converters for common parameter types:
# Built-in converters
urlpatterns = [
# String (default) - matches any non-empty string, excluding '/'
path('posts/<str:title>/', views.post_by_title),
# Integer - matches zero or any positive integer
path('posts/<int:pk>/', views.post_detail),
# Slug - matches any slug string (letters, numbers, hyphens, underscores)
path('posts/<slug:slug>/', views.post_by_slug),
# UUID - matches a formatted UUID
path('posts/<uuid:id>/', views.post_by_uuid),
# Path - matches any non-empty string, including '/'
path('files/<path:file_path>/', views.serve_file),
]
# Example views
def post_by_title(request, title):
# title is a string
post = get_object_or_404(Post, title=title)
return render(request, 'blog/post_detail.html', {'post': post})
def post_detail(request, pk):
# pk is an integer
post = get_object_or_404(Post, pk=pk)
return render(request, 'blog/post_detail.html', {'post': post})
def post_by_slug(request, slug):
# slug is a slug string
post = get_object_or_404(Post, slug=slug)
return render(request, 'blog/post_detail.html', {'post': post})
def serve_file(request, file_path):
# file_path can contain forward slashes
# e.g., 'documents/2024/report.pdf'
return serve_static_file(file_path)
# converters.py
class YearConverter:
regex = '[0-9]{4}'
def to_python(self, value):
return int(value)
def to_url(self, value):
return str(value)
class MonthConverter:
regex = '(0?[1-9]|1[0-2])' # 1-12 or 01-12
def to_python(self, value):
return int(value)
def to_url(self, value):
return f'{value:02d}'
class CategoryConverter:
regex = '[a-zA-Z0-9-_]+'
def to_python(self, value):
# Convert to Category object
from blog.models import Category
try:
return Category.objects.get(slug=value)
except Category.DoesNotExist:
raise ValueError("Category not found")
def to_url(self, value):
if hasattr(value, 'slug'):
return value.slug
return str(value)
# Register converters
from django.urls import register_converter
register_converter(YearConverter, 'year')
register_converter(MonthConverter, 'month')
register_converter(CategoryConverter, 'category')
# Usage in URLs
urlpatterns = [
path('archive/<year:year>/', views.posts_by_year),
path('archive/<year:year>/<month:month>/', views.posts_by_month),
path('category/<category:cat>/', views.posts_by_category),
]
For complex pattern matching, use re_path:
from django.urls import re_path
urlpatterns = [
# Complex date pattern
re_path(
r'^archive/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<day>[0-9]{2})/$',
views.posts_by_date,
name='posts_by_date'
),
# Version-specific API
re_path(
r'^api/v(?P<version>[1-9]\d*)/',
include('api.urls'),
name='api_versioned'
),
# File extension matching
re_path(
r'^download/(?P<filename>[\w\-_]+\.(?P<extension>pdf|doc|docx))/$',
views.download_file,
name='download_file'
),
# Optional parameters
re_path(
r'^search/(?P<query>[\w\s]+)(?:/page/(?P<page>\d+))?/$',
views.search_posts,
name='search_posts'
),
# Multiple format support
re_path(
r'^posts/(?P<pk>\d+)\.(?P<format>json|xml|html)$',
views.post_detail_formatted,
name='post_detail_formatted'
),
]
# Corresponding views
def posts_by_date(request, year, month, day):
# All parameters are strings from regex groups
year, month, day = int(year), int(month), int(day)
date = datetime.date(year, month, day)
posts = Post.objects.filter(created_at__date=date)
return render(request, 'blog/posts_by_date.html', {
'posts': posts,
'date': date
})
def download_file(request, filename, extension):
# filename: 'document-name'
# extension: 'pdf'
file_path = f'downloads/{filename}.{extension}'
return serve_file(request, file_path)
def search_posts(request, query, page=None):
# page is optional and may be None
page = int(page) if page else 1
posts = Post.objects.filter(title__icontains=query)
paginator = Paginator(posts, 10)
posts_page = paginator.get_page(page)
return render(request, 'blog/search_results.html', {
'posts': posts_page,
'query': query
})
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog' # Define namespace
urlpatterns = [
path('', views.post_list, name='list'),
path('<int:pk>/', views.post_detail, name='detail'),
path('create/', views.post_create, name='create'),
path('<int:pk>/edit/', views.post_edit, name='edit'),
]
# shop/urls.py
from django.urls import path
from . import views
app_name = 'shop' # Same name pattern, different namespace
urlpatterns = [
path('', views.product_list, name='list'),
path('<int:pk>/', views.product_detail, name='detail'),
path('create/', views.product_create, name='create'),
]
# myproject/urls.py
from django.urls import path, include
urlpatterns = [
# Different instances of the same app
path('blog/', include('blog.urls', namespace='blog')),
path('news/', include('blog.urls', namespace='news')),
path('admin-blog/', include('blog.urls', namespace='admin_blog')),
]
# In views or templates, reference with namespace
from django.urls import reverse
# Reverse URL with namespace
blog_url = reverse('blog:detail', args=[post.pk])
news_url = reverse('news:detail', args=[post.pk])
admin_url = reverse('admin_blog:detail', args=[post.pk])
# api/urls.py
from django.urls import path, include
app_name = 'api'
urlpatterns = [
path('v1/', include('api.v1.urls', namespace='v1')),
path('v2/', include('api.v2.urls', namespace='v2')),
]
# api/v1/urls.py
from django.urls import path, include
app_name = 'v1'
urlpatterns = [
path('blog/', include('api.v1.blog_urls', namespace='blog')),
path('users/', include('api.v1.user_urls', namespace='users')),
]
# Usage: api:v1:blog:list
api_blog_list_url = reverse('api:v1:blog:list')
# myproject/urls.py
from django.urls import path, include
urlpatterns = [
# Include entire app URLs
path('blog/', include('blog.urls')),
# Include with additional patterns
path('api/', include([
path('v1/', include('api.v1.urls')),
path('v2/', include('api.v2.urls')),
path('auth/', include('api.auth.urls')),
])),
# Include with conditions
path('debug/', include('debug_toolbar.urls')) if settings.DEBUG else path(''),
]
# myproject/urls.py
from django.conf import settings
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('core.urls')),
]
# Development-only URLs
if settings.DEBUG:
urlpatterns += [
path('__debug__/', include('debug_toolbar.urls')),
path('silk/', include('silk.urls')),
]
# Feature flags
if getattr(settings, 'ENABLE_API', False):
urlpatterns += [
path('api/', include('api.urls')),
]
# Environment-specific URLs
if settings.ENVIRONMENT == 'staging':
urlpatterns += [
path('test/', include('testing.urls')),
]
# utils/urls.py
from django.urls import path
from importlib import import_module
def generate_app_urls(app_configs):
"""Dynamically generate URL patterns for multiple apps"""
patterns = []
for app_config in app_configs:
app_name = app_config['name']
url_prefix = app_config.get('prefix', app_name)
try:
app_urls = import_module(f'{app_name}.urls')
patterns.append(
path(f'{url_prefix}/', include(app_urls, namespace=app_name))
)
except ImportError:
continue
return patterns
# myproject/urls.py
from utils.urls import generate_app_urls
INSTALLED_APPS_CONFIG = [
{'name': 'blog', 'prefix': 'blog'},
{'name': 'shop', 'prefix': 'store'},
{'name': 'forum', 'prefix': 'community'},
]
urlpatterns = [
path('admin/', admin.site.urls),
] + generate_app_urls(INSTALLED_APPS_CONFIG)
from django.urls import reverse
from django.shortcuts import redirect
# In views
def post_create(request):
if request.method == 'POST':
# Process form
post = create_post(request.POST)
# Redirect to detail view
return redirect('blog:detail', pk=post.pk)
return render(request, 'blog/post_form.html')
# With arguments
def redirect_to_post(request, post_id):
return redirect('blog:detail', pk=post_id)
# With keyword arguments
def redirect_to_date_archive(request):
return redirect('blog:archive', year=2024, month=3)
# Reverse to get URL string
def get_post_url(post):
return reverse('blog:detail', kwargs={'pk': post.pk})
<!-- Basic URL reversing -->
<a href="{% url 'blog:list' %}">All Posts</a>
<a href="{% url 'blog:detail' pk=post.pk %}">{{ post.title }}</a>
<a href="{% url 'blog:edit' post.pk %}">Edit</a>
<!-- With multiple parameters -->
<a href="{% url 'blog:archive' year=2024 month=3 %}">March 2024</a>
<!-- Dynamic parameters -->
{% for post in posts %}
<a href="{% url 'blog:detail' pk=post.pk %}">{{ post.title }}</a>
{% endfor %}
<!-- Conditional URLs -->
{% if user.is_authenticated %}
<a href="{% url 'blog:create' %}">Create Post</a>
{% else %}
<a href="{% url 'accounts:login' %}">Login to Create</a>
{% endif %}
<!-- URL with query parameters -->
<a href="{% url 'blog:list' %}?category={{ category.slug }}">{{ category.name }}</a>
# utils/urls.py
from django.urls import reverse
from django.http import QueryDict
def reverse_with_query(viewname, args=None, kwargs=None, query_params=None):
"""Reverse URL with query parameters"""
url = reverse(viewname, args=args, kwargs=kwargs)
if query_params:
query_dict = QueryDict(mutable=True)
query_dict.update(query_params)
url += f'?{query_dict.urlencode()}'
return url
# Usage
search_url = reverse_with_query(
'blog:list',
query_params={'q': 'django', 'category': 'tech', 'page': 2}
)
# Result: /blog/?q=django&category=tech&page=2
# Class-based URL builder
class URLBuilder:
def __init__(self, viewname, namespace=None):
self.viewname = f'{namespace}:{viewname}' if namespace else viewname
self.args = []
self.kwargs = {}
self.query_params = {}
def arg(self, *args):
self.args.extend(args)
return self
def kwarg(self, **kwargs):
self.kwargs.update(kwargs)
return self
def query(self, **params):
self.query_params.update(params)
return self
def build(self):
return reverse_with_query(
self.viewname,
args=self.args,
kwargs=self.kwargs,
query_params=self.query_params
)
# Usage
url = (URLBuilder('detail', namespace='blog')
.kwarg(pk=123)
.query(ref='homepage', utm_source='email')
.build())
# Efficient URL patterns
urlpatterns = [
# Most specific patterns first
path('posts/create/', views.post_create, name='create'),
path('posts/popular/', views.popular_posts, name='popular'),
path('posts/recent/', views.recent_posts, name='recent'),
# Generic patterns last
path('posts/<int:pk>/', views.post_detail, name='detail'),
path('posts/<slug:slug>/', views.post_by_slug, name='by_slug'),
]
# Avoid overlapping patterns
# BAD:
urlpatterns = [
path('<str:category>/', views.category_posts), # Too broad
path('create/', views.post_create), # Will never match
]
# GOOD:
urlpatterns = [
path('create/', views.post_create), # Specific first
path('<str:category>/', views.category_posts), # Generic last
]
# tests/test_urls.py
from django.test import TestCase
from django.urls import reverse, resolve
from django.contrib.auth.models import User
from blog.models import Post
class URLTests(TestCase):
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
self.post = Post.objects.create(
title='Test Post',
slug='test-post',
content='Test content',
author=self.user
)
def test_post_list_url(self):
"""Test post list URL resolves correctly"""
url = reverse('blog:list')
self.assertEqual(url, '/blog/')
resolver = resolve('/blog/')
self.assertEqual(resolver.view_name, 'blog:list')
def test_post_detail_url(self):
"""Test post detail URL with pk"""
url = reverse('blog:detail', kwargs={'pk': self.post.pk})
self.assertEqual(url, f'/blog/{self.post.pk}/')
resolver = resolve(f'/blog/{self.post.pk}/')
self.assertEqual(resolver.view_name, 'blog:detail')
self.assertEqual(resolver.kwargs['pk'], str(self.post.pk))
def test_post_slug_url(self):
"""Test post detail URL with slug"""
url = reverse('blog:by_slug', kwargs={'slug': self.post.slug})
self.assertEqual(url, f'/blog/{self.post.slug}/')
def test_invalid_urls(self):
"""Test invalid URLs return 404"""
response = self.client.get('/blog/999999/')
self.assertEqual(response.status_code, 404)
response = self.client.get('/blog/invalid-slug/')
self.assertEqual(response.status_code, 404)
def test_url_parameters(self):
"""Test URL parameter extraction"""
resolver = resolve(f'/blog/{self.post.pk}/')
self.assertIn('pk', resolver.kwargs)
self.assertEqual(int(resolver.kwargs['pk']), self.post.pk)
Django's URL dispatcher provides powerful, flexible routing capabilities. Understanding pattern matching, namespaces, and URL organization enables you to build maintainable, scalable URL structures that support complex application requirements while maintaining clean, readable URLs.
URLs and Views
Django's URL dispatcher and view system form the core of request handling in web applications. This chapter covers everything from basic URL patterns to advanced view techniques, providing the foundation for building robust, scalable web applications.
Writing Function-Based Views
Function-based views (FBVs) are the foundation of Django's view system. They provide direct, explicit control over request handling and are ideal for custom logic, simple operations, and learning Django concepts.