Difficulty: Advanced
DRF routers automatically generate URL patterns for ViewSets. Instead of manually defining URL patterns for each action (list, create, retrieve, update, delete), you register ViewSets with a router and it creates all the necessary URL patterns.
DRF provides two main routers: SimpleRouter and DefaultRouter. DefaultRouter extends SimpleRouter by adding a root API view that lists all registered endpoints. This root view provides a browsable API index at the base URL.
To use a router, create an instance, register ViewSets with URL prefixes, and include the router's URLs in your URL configuration. router.register('articles', ArticleViewSet) creates URL patterns for /articles/ (list/create) and /articles/{pk}/ (retrieve/update/delete).
Custom actions extend ViewSets with additional endpoints beyond standard CRUD. Use the @action decorator to define custom actions. By default, actions respond to POST requests and use the ViewSet's detail URL ({pk}/action-name/). Set detail=False for collection-level actions (/articles/action-name/).
The @action decorator accepts parameters like methods (HTTP methods), detail (True for instance-level, False for collection-level), url_path (custom URL segment), url_name (for URL reversing), and permission_classes (action-specific permissions).
Routers also support nested routing through third-party packages like drf-nested-routers, which creates URL patterns like /authors/{author_pk}/articles/ for nested resources.
# api/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register('articles', views.ArticleViewSet)
router.register('categories', views.CategoryViewSet)
router.register('tags', views.TagViewSet, basename='tag')
urlpatterns = [
path('api/', include(router.urls)),
]
# Generated URLs:
# GET/POST /api/articles/ -> list/create
# GET/PUT/DELETE /api/articles/{pk}/ -> retrieve/update/destroy
# GET/POST /api/categories/ -> list/create
# GET/PUT/DELETE /api/categories/{pk}/ -> retrieve/update/destroy
# GET /api/tags/ -> list
# GET /api/tags/{pk}/ -> retrieve
# GET /api/ -> API root (DefaultRouter only)
DefaultRouter auto-generates URL patterns from registered ViewSets. Use basename when the ViewSet does not have a queryset attribute. The API root view at /api/ lists all endpoints.
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Article
from .serializers import ArticleSerializer
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# Detail action: POST /articles/{pk}/publish/
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
article = self.get_object()
article.is_published = True
article.save()
return Response({'status': 'Article published'})
# Detail action: POST /articles/{pk}/archive/
@action(detail=True, methods=['post'], url_path='archive')
def archive(self, request, pk=None):
article = self.get_object()
article.status = 'archived'
article.save()
return Response({'status': 'Article archived'})
# Collection action: GET /articles/featured/
@action(detail=False, methods=['get'])
def featured(self, request):
featured = self.get_queryset().filter(is_featured=True)[:10]
serializer = self.get_serializer(featured, many=True)
return Response(serializer.data)
# Collection action: GET /articles/stats/
@action(detail=False, methods=['get'], url_path='stats')
def statistics(self, request):
qs = self.get_queryset()
return Response({
'total': qs.count(),
'published': qs.filter(is_published=True).count(),
'draft': qs.filter(is_published=False).count(),
})
detail=True creates /articles/{pk}/action/. detail=False creates /articles/action/. url_path customizes the URL segment. self.get_object() gets the instance for detail actions. self.get_queryset() gets the queryset for collection actions.
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register('articles', views.ArticleViewSet)
router.register('categories', views.CategoryViewSet)
urlpatterns = [
# Router-generated URLs
path('api/v1/', include(router.urls)),
# Manual API URLs (for non-ViewSet views)
path('api/v1/auth/login/', views.LoginAPIView.as_view(), name='api-login'),
path('api/v1/auth/register/', views.RegisterAPIView.as_view(), name='api-register'),
path('api/v1/search/', views.SearchAPIView.as_view(), name='api-search'),
# DRF browsable API authentication
path('api-auth/', include('rest_framework.urls')),
]
Router URLs and manual URL patterns can coexist. Use manual URLs for non-ViewSet views (like authentication endpoints). Include rest_framework.urls for the browsable API login/logout buttons.
# api/v1/urls.py
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register('articles', views.ArticleViewSet)
router.register('users', views.UserViewSet)
urlpatterns = router.urls
# api/v2/urls.py
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register('articles', views.ArticleV2ViewSet)
router.register('users', views.UserV2ViewSet)
urlpatterns = router.urls
# project/urls.py
from django.urls import path, include
urlpatterns = [
path('api/v1/', include('api.v1.urls')),
path('api/v2/', include('api.v2.urls')),
]
API versioning through URL prefixes uses separate routers for each version. Each version can have different ViewSets and serializers while maintaining backward compatibility.
DefaultRouter, SimpleRouter, register, Custom Actions, action Decorator