Skip to content

Chapter 3: URL Routing System

3.2 Django Advanced URL Features

Named URLs and Reverse Resolution

Why Named URLs Are Needed

Hardcoding URL paths in templates and views is bad practice because:

  1. Maintenance difficulty: Need to modify multiple places when URLs change
  2. Error prone: Manual URL writing is prone to typos
  3. Inflexible: Hard to adapt to URL structure changes

Django provides named URLs and reverse resolution mechanisms to solve these problems.

Named URL Examples

python
# blog/urls.py
from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [
    path('', views.home, name='home'),
    path('articles/', views.article_list, name='article_list'),
    path('articles/<slug:slug>/', views.article_detail, name='article_detail'),
    path('categories/', views.category_list, name='category_list'),
    path('categories/<slug:slug>/', views.category_detail, name='category_detail'),
    path('authors/<str:username>/', views.author_profile, name='author_profile'),
    path('search/', views.search, name='search'),
    path('archive/<int:year>/', views.year_archive, name='year_archive'),
    path('archive/<int:year>/<int:month>/', views.month_archive, name='month_archive'),
    path('rss.xml', views.rss_feed, name='rss_feed'),
]

Using Reverse Resolution in Views

python
# views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse, reverse_lazy
from django.http import HttpResponseRedirect
from .models import Article, Category
from .forms import ArticleForm

def create_article(request):
    """Create article view"""
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save(commit=False)
            article.author = request.user
            article.save()
            
            # Use reverse for redirection
            return redirect('blog:article_detail', slug=article.slug)
    else:
        form = ArticleForm()
    
    return render(request, 'blog/create_article.html', {'form': form})

def article_published(request, article_id):
    """Handle after article publication"""
    article = get_object_or_404(Article, id=article_id)
    article.status = 'published'
    article.save()
    
    # Construct redirect URL
    redirect_url = reverse('blog:article_detail', kwargs={'slug': article.slug})
    return HttpResponseRedirect(redirect_url)

# Using reverse_lazy in class views
from django.views.generic import CreateView
from django.urls import reverse_lazy

class ArticleCreateView(CreateView):
    model = Article
    form_class = ArticleForm
    template_name = 'blog/create_article.html'
    success_url = reverse_lazy('blog:article_list')  # Use reverse_lazy
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)
    
    def get_success_url(self):
        """Dynamically get success URL"""
        return reverse('blog:article_detail', kwargs={'slug': self.object.slug})
reverse_lazy Explanation

reverse_lazy is the lazy loading version of the reverse function. Its core features:

  • ​Delayed resolution​: Only actually executes URL parsing when the string value is really needed (e.g., when the view processes the request). This avoids django.core.exceptions.ImproperlyConfigured exceptions that may occur when trying to parse URLs before Django's URL configuration is ready (such as during class definition, module loading).
  • ​Used for class attribute definition​: Because of its lazy nature, it is particularly suitable for defining as class attributes in Class-Based Views (CBV), such as success_url.

Remember a simple principle: ​When you need to define URLs at the class attribute or module level, prioritize reverse_lazy; use reverse inside view functions or methods.

Specifying success_url in class views​: This is the most common case. When an object is successfully deleted, you want to redirect to a certain URL. If reverse is used here, Django may report an error when loading this class at startup because the URL configuration may not be fully loaded yet.

python
from django.views.generic.edit import DeleteView
from django.urls import reverse_lazy
from .models import Article

class ArticleDeleteView(DeleteView):
    model = Article
    success_url = reverse_lazy('article_list')  # Using reverse_lazy here is safe

Using URL Tags in Templates

html
<!-- blog/templates/blog/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <title>{% block title %}My Blog{% endblock %}</title>
</head>
<body>
    <nav class="navbar">
        <div class="container">
            <a class="navbar-brand" href="{% url 'blog:home' %}">My Blog</a>
            <ul class="navbar-nav">
                <li><a href="{% url 'blog:home' %}">Home</a></li>
                <li><a href="{% url 'blog:article_list' %}">Articles</a></li>
                <li><a href="{% url 'blog:category_list' %}">Categories</a></li>
                <li><a href="{% url 'blog:search' %}">Search</a></li>
            </ul>
        </div>
    </nav>
    
    <main class="container">
        {% block content %}
        {% endblock %}
    </main>
    
    <footer>
        <p><a href="{% url 'blog:rss_feed' %}">RSS Feed</a></p>
    </footer>
</body>
</html>
html
<!-- blog/templates/blog/article_list.html -->
{% extends 'blog/base.html' %}

{% block content %}
<h2>Article List</h2>

{% for article in articles %}
<article class="article-card">
    <h3>
        <a href="{% url 'blog:article_detail' slug=article.slug %}">
            {{ article.title }}
        </a>
    </h3>
    <p class="meta">
        Author: <a href="{% url 'blog:author_profile' username=article.author.username %}">
            {{ article.author.username }}
        </a>
        {% if article.category %}
        | Category: <a href="{% url 'blog:category_detail' slug=article.category.slug %}">
            {{ article.category.name }}
        </a>
        {% endif %}
        | Publish Time: {{ article.publish_date|date:"Y-m-d" }}
    </p>
    <p>{{ article.excerpt }}</p>
    <a href="{% url 'blog:article_detail' slug=article.slug %}" class="read-more">
        Read More
    </a>
</article>
{% empty %}
<p>No articles published yet.</p>
{% endfor %}

<!-- Pagination links -->
{% if is_paginated %}
<div class="pagination">
    {% if page_obj.has_previous %}
    <a href="{% url 'blog:article_list' %}?page={{ page_obj.previous_page_number }}">
        Previous
    </a>
    {% endif %}
    
    {% if page_obj.has_next %}
    <a href="{% url 'blog:article_list' %}?page={{ page_obj.next_page_number }}">
        Next
    </a>
    {% endif %}
</div>
{% endif %}
{% endblock %}

URL Reverse Resolution with Parameters

html
<!-- Pass multiple parameters in template -->
{% url 'blog:monthly_archive' year=2023 month=12 %}

<!-- Use variables as parameters -->
{% url 'blog:article_detail' slug=article.slug %}

<!-- Complex URL parameters -->
{% url 'blog:article_detail' year=article.publish_date.year month=article.publish_date.month day=article.publish_date.day slug=article.slug %}
python
# Pass parameters in Python code
from django.urls import reverse

# Single parameter
url = reverse('blog:article_detail', args=[article.slug])
# Or use keyword arguments
url = reverse('blog:article_detail', kwargs={'slug': article.slug})

# Multiple parameters
url = reverse('blog:monthly_archive', kwargs={
    'year': 2023,
    'month': 12
})

# Complex parameters
url = reverse('blog:article_detail', kwargs={
    'year': article.publish_date.year,
    'month': article.publish_date.month,
    'day': article.publish_date.day,
    'slug': article.slug
})

URL Namespaces

Application Namespace

Using application namespaces can avoid URL name conflicts between different applications. For example, when both blog home and news home have 'home', you can use application namespaces to distinguish them.

python
# blog/urls.py
from django.urls import path
from . import views

app_name = 'blog'  # Application namespace

urlpatterns = [
    path('', views.home, name='home'),
    path('articles/', views.article_list, name='article_list'),
    # ...
]
python
# news/urls.py
from django.urls import path
from . import views

app_name = 'news'  # Application namespace

urlpatterns = [
    path('', views.home, name='home'),  # Doesn't conflict with blog app's home
    path('articles/', views.article_list, name='article_list'),
    # ...
]

Instance Namespace

Instance namespaces are used for multiple instances of the same application:

python
# mysite/urls.py
from django.urls import path, include

urlpatterns = [
    path('blog/', include('blog.urls', namespace='blog')),
    path('news/', include('news.urls', namespace='news')),  # Same application, different instances
    path('admin/', admin.site.urls),
]

Using Namespaces

python
# Using namespaces in views
from django.urls import reverse

def some_view(request):
    # Use application namespace
    blog_url = reverse('blog:article_list')
    news_url = reverse('news:article_list')
    
    # Use current namespace
    current_namespace = request.resolver_match.namespace
    url = reverse(f'{current_namespace}:home')
    
    return redirect(url)
html
<!-- Using namespaces in templates -->
<a href="{% url 'blog:home' %}">Blog Home</a>
<a href="{% url 'news:home' %}">News Home</a>

<!-- Dynamic namespace -->
{% with current_app as app_name %}
<a href="{% url app_name|add:':home' %}">Home</a>
{% endwith %}

Including Other URLconfs

Basic Inclusion

python
# mysite/urls.py (main URLconf)
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
    path('api/', include('api.urls')),
    path('users/', include('users.urls')),
    path('', include('core.urls')),  # Root path
]

Inclusion with Namespace

python
# mysite/urls.py
from django.urls import path, include

urlpatterns = [
    path('blog/', include('blog.urls', namespace='blog')),
    path('api/v1/', include('api.urls', namespace='api-v1')),
    path('api/v2/', include('api.urls', namespace='api-v2')),
]

Conditional Inclusion

python
# mysite/urls.py
from django.conf import settings
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
]

# Development environment specific URLs
if settings.DEBUG:
    import debug_toolbar
    urlpatterns += [
        path('__debug__/', include(debug_toolbar.urls)),
    ]
    
    # Media file serving (development only)
    from django.conf.urls.static import static
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

# API documentation (only in specific environments)
if getattr(settings, 'ENABLE_API_DOCS', False):
    urlpatterns += [
        path('api-docs/', include('rest_framework.urls')),
    ]

Nested Inclusion

python
# 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')),
]
python
# api/v1/urls.py
from django.urls import path
from . import views

app_name = 'v1'

urlpatterns = [
    path('articles/', views.ArticleListAPIView.as_view(), name='article_list'),
    path('articles/<int:id>/', views.ArticleDetailAPIView.as_view(), name='article_detail'),
]

Using nested namespaces:

python
# Access nested URLs
url = reverse('api:v1:article_list')
# Result: /api/v1/articles/

Practical Example: User Profile Page Routing

Let's demonstrate advanced URL features through a complete user profile page routing system:

1. User Application URL Configuration

python
# users/urls.py
from django.urls import path, include
from . import views

app_name = 'users'

# User profile related URLs
profile_patterns = [
    path('', views.ProfileView.as_view(), name='profile'),
    path('edit/', views.ProfileEditView.as_view(), name='profile_edit'),
    path('avatar/', views.AvatarUpdateView.as_view(), name='avatar_update'),
    path('settings/', views.SettingsView.as_view(), name='settings'),
]

# User content related URLs
content_patterns = [
    path('articles/', views.UserArticlesView.as_view(), name='articles'),
    path('comments/', views.UserCommentsView.as_view(), name='comments'),
    path('favorites/', views.UserFavoritesView.as_view(), name='favorites'),
    path('drafts/', views.UserDraftsView.as_view(), name='drafts'),
]

urlpatterns = [
    # User list and search
    path('', views.UserListView.as_view(), name='list'),
    path('search/', views.UserSearchView.as_view(), name='search'),
    
    # Authentication related
    path('login/', views.LoginView.as_view(), name='login'),
    path('logout/', views.LogoutView.as_view(), name='logout'),
    path('register/', views.RegisterView.as_view(), name='register'),
    path('password-reset/', views.PasswordResetView.as_view(), name='password_reset'),
    
    # User profile pages (include sub-routes)
    path('<str:username>/', views.UserDetailView.as_view(), name='detail'),
    path('<str:username>/profile/', include(profile_patterns)),
    path('<str:username>/content/', include(content_patterns)),
    
    # Follow system
    path('<str:username>/follow/', views.FollowUserView.as_view(), name='follow'),
    path('<str:username>/unfollow/', views.UnfollowUserView.as_view(), name='unfollow'),
    path('<str:username>/followers/', views.UserFollowersView.as_view(), name='followers'),
    path('<str:username>/following/', views.UserFollowingView.as_view(), name='following'),
]

2. Corresponding View Implementation

python
# users/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import DetailView, ListView, UpdateView
from django.urls import reverse, reverse_lazy
from django.contrib.auth.models import User
from django.http import JsonResponse
from .models import Profile, Follow
from .forms import ProfileForm

class UserDetailView(DetailView):
    """User detail page"""
    model = User
    template_name = 'users/user_detail.html'
    context_object_name = 'user'
    slug_field = 'username'
    slug_url_kwarg = 'username'
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user = self.get_object()
        
        # Add user statistics
        context.update({
            'article_count': user.article_set.filter(status='published').count(),
            'follower_count': user.followers.count(),
            'following_count': user.following.count(),
            'is_following': self.request.user.is_authenticated and 
                          Follow.objects.filter(
                              follower=self.request.user, 
                              following=user
                          ).exists(),
        })
        return context

class ProfileView(LoginRequiredMixin, DetailView):
    """User profile page"""
    model = User
    template_name = 'users/profile.html'
    context_object_name = 'profile_user'
    slug_field = 'username'
    slug_url_kwarg = 'username'
    
    def get_object(self):
        username = self.kwargs.get('username')
        return get_object_or_404(User, username=username)

class ProfileEditView(LoginRequiredMixin, UpdateView):
    """Edit profile"""
    model = Profile
    form_class = ProfileForm
    template_name = 'users/profile_edit.html'
    
    def get_object(self):
        username = self.kwargs.get('username')
        user = get_object_or_404(User, username=username)
        # Ensure only editing own profile
        if user != self.request.user:
            raise PermissionDenied("You can only edit your own profile")
        return user.profile
    
    def get_success_url(self):
        return reverse('users:profile', kwargs={'username': self.request.user.username})

class FollowUserView(LoginRequiredMixin, View):
    """Follow user"""
    def post(self, request, username):
        target_user = get_object_or_404(User, username=username)
        
        if target_user == request.user:
            return JsonResponse({'error': 'Cannot follow yourself'}, status=400)
        
        follow, created = Follow.objects.get_or_create(
            follower=request.user,
            following=target_user
        )
        
        if created:
            return JsonResponse({
                'status': 'followed',
                'message': f'Followed {username}',
                'follower_count': target_user.followers.count()
            })
        else:
            return JsonResponse({
                'status': 'already_following',
                'message': f'Already following {username}'
            })

3. URL Usage in Templates

html
<!-- users/templates/users/user_detail.html -->
{% extends 'base.html' %}

{% block content %}
<div class="user-profile">
    <div class="user-header">
        <img src="{{ user.profile.avatar.url }}" alt="{{ user.username }}" class="avatar">
        <div class="user-info">
            <h1>{{ user.get_full_name|default:user.username }}</h1>
            <p>@{{ user.username }}</p>
            
            <div class="user-stats">
                <a href="{% url 'users:articles' username=user.username %}">
                    <strong>{{ article_count }}</strong> Articles
                </a>
                <a href="{% url 'users:followers' username=user.username %}">
                    <strong>{{ follower_count }}</strong> Followers
                </a>
                <a href="{% url 'users:following' username=user.username %}">
                    <strong>{{ following_count }}</strong> Following
                </a>
            </div>
            
            {% if user == request.user %}
            <!-- User's own page -->
            <div class="user-actions">
                <a href="{% url 'users:profile' username=user.username %}" class="btn btn-primary">
                    View Profile
                </a>
                <a href="{% url 'users:profile_edit' username=user.username %}" class="btn btn-secondary">
                    Edit Profile
                </a>
                <a href="{% url 'users:drafts' username=user.username %}" class="btn btn-outline-primary">
                    Drafts
                </a>
            </div>
            {% else %}
            <!-- Other user's page -->
            <div class="user-actions">
                {% if is_following %}
                <button class="btn btn-outline-primary" onclick="unfollowUser('{{ user.username }}')">
                    Following
                </button>
                {% else %}
                <button class="btn btn-primary" onclick="followUser('{{ user.username }}')">
                    Follow
                </button>
                {% endif %}
            </div>
            {% endif %}
        </div>
    </div>
    
    <div class="user-content">
        <nav class="nav nav-tabs">
            <a class="nav-link active" href="{% url 'users:detail' username=user.username %}">
                Overview
            </a>
            <a class="nav-link" href="{% url 'users:articles' username=user.username %}">
                Articles
            </a>
            <a class="nav-link" href="{% url 'users:comments' username=user.username %}">
                Comments
            </a>
            {% if user == request.user %}
            <a class="nav-link" href="{% url 'users:favorites' username=user.username %}">
                Favorites
            </a>
            {% endif %}
        </nav>
        
        <div class="tab-content">
            <!-- User overview content -->
        </div>
    </div>
</div>

<script>
function followUser(username) {
    fetch(`{% url 'users:follow' username='__USERNAME__' %}`.replace('__USERNAME__', username), {
        method: 'POST',
        headers: {
            'X-CSRFToken': '{{ csrf_token }}',
            'Content-Type': 'application/json'
        }
    })
    .then(response => response.json())
    .then(data => {
        if (data.status === 'followed') {
            location.reload();
        }
    });
}

function unfollowUser(username) {
    fetch(`{% url 'users:unfollow' username='__USERNAME__' %}`.replace('__USERNAME__', username), {
        method: 'POST',
        headers: {
            'X-CSRFToken': '{{ csrf_token }}',
            'Content-Type': 'application/json'
        }
    })
    .then(response => response.json())
    .then(data => {
        if (data.status === 'unfollowed') {
            location.reload();
        }
    });
}
</script>
{% endblock %}

4. Include in Main URLconf

python
# mysite/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blog.urls')),
    path('users/', include('users.urls')),
    path('api/', include('api.urls')),
]

URL Debugging and Testing

1. URL Reverse Resolution Testing

python
# tests/test_urls.py
from django.test import TestCase
from django.urls import reverse, resolve
from django.contrib.auth.models import User
from users.views import UserDetailView

class URLTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpass'
        )
    
    def test_user_detail_url_reverse(self):
        """Test user detail page URL reverse resolution"""
        url = reverse('users:detail', kwargs={'username': 'testuser'})
        self.assertEqual(url, '/users/testuser/')
    
    def test_user_detail_url_resolve(self):
        """Test URL resolves to correct view"""
        resolver = resolve('/users/testuser/')
        self.assertEqual(resolver.view_class, UserDetailView)
        self.assertEqual(resolver.kwargs, {'username': 'testuser'})
    
    def test_nested_urls(self):
        """Test nested URLs"""
        url = reverse('users:profile', kwargs={'username': 'testuser'})
        self.assertEqual(url, '/users/testuser/profile/')
        
        url = reverse('users:articles', kwargs={'username': 'testuser'})
        self.assertEqual(url, '/users/testuser/content/articles/')

2. URL Performance Testing

python
# tests/test_url_performance.py
from django.test import TestCase
from django.urls import reverse
from django.test.utils import override_settings
import time

class URLPerformanceTests(TestCase):
    def test_url_reverse_performance(self):
        """Test URL reverse resolution performance"""
        start_time = time.time()
        
        for i in range(1000):
            url = reverse('users:detail', kwargs={'username': f'user{i}'})
        
        end_time = time.time()
        duration = end_time - start_time
        
        # Ensure 1000 reverse resolutions complete in reasonable time
        self.assertLess(duration, 1.0, "URL reverse resolution too slow")

Summary

Django's advanced URL features provide powerful and flexible URL management mechanisms:

  1. Named URLs: Avoid hardcoding, improve maintainability
  2. Reverse resolution: Generate URLs by name, support parameter passing
  3. Namespaces: Avoid URL name conflicts, support application and instance namespaces
  4. Including URLconfs: Implement modular URL configuration
  5. Conditional inclusion: Dynamically configure URLs based on environment

Best practices:

  • Always name URL patterns
  • Use application namespaces to avoid conflicts
  • Use {% url %} tags in templates
  • Use reverse() function in views
  • Write URL tests to ensure correct configuration

Next Article

We will learn Django's model system in depth, which is the data foundation of Django applications.

4.1 Django Data Model Basics →

Directory

Return to Course Directory

Released under the [BY-NC-ND License](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.en).