DRF ViewSets and Routers: Simplifying API Development

ViewSets and Routers are Django REST Framework's most powerful abstractions combining related views into single classes with automatic URL routing eliminating repetitive code for standard CRUD operations. While basic DRF views require separate classes or functions for list, create, retrieve, update, and delete operations with manual URL configuration, ViewSets consolidate these operations into cohesive classes with routers automatically generating URL patterns following RESTful conventions. This consolidation reduces code duplication, enforces consistency across API endpoints, simplifies maintenance, and accelerates development enabling developers to build complete CRUD APIs with minimal boilerplate. ViewSets build upon serializers and generic views providing the highest level of abstraction in DRF's view hierarchy trading some flexibility for dramatic productivity gains. This comprehensive guide explores DRF ViewSets and Routers including understanding ViewSet types and their purposes, implementing ModelViewSet for complete CRUD operations, using ReadOnlyModelViewSet for read-only APIs, creating custom ViewSets with additional actions, configuring DefaultRouter and SimpleRouter for automatic URL generation, implementing custom actions with action decorator, handling nested routes and relationships, versioning APIs with routers, optimizing ViewSet queries with select_related and prefetch_related, and best practices for ViewSet design. Mastering ViewSets and Routers enables building sophisticated REST APIs rapidly while maintaining clean organized code throughout Django REST Framework development.
Understanding ViewSets
ViewSets combine the logic for handling multiple related views into a single class eliminating the need for separate list, create, detail, update, and delete views. Unlike traditional class-based views that map one class to one URL pattern, ViewSets map one class to multiple URLs through routers that automatically generate URL configurations. The ViewSet base class provides no actions by itself while its subclasses GenericViewSet, ReadOnlyModelViewSet, and ModelViewSet include increasingly complete sets of operations. Understanding ViewSet inheritance hierarchy helps choose appropriate base classes balancing code reduction against customization needs. ViewSets work with the same Django models and serializers as other DRF views inheriting familiar patterns while adding router integration.
# Basic ViewSet example
from rest_framework import viewsets
from rest_framework.response import Response
from .models import Article
from .serializers import ArticleSerializer
# ReadOnlyModelViewSet - List and retrieve only
class ArticleViewSet(viewsets.ReadOnlyModelViewSet):
"""
API endpoint for viewing articles (read-only)
Automatically provides:
- list() -> GET /articles/
- retrieve() -> GET /articles/{pk}/
"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# ModelViewSet - Complete CRUD operations
class ArticleViewSet(viewsets.ModelViewSet):
"""
API endpoint for managing articles (full CRUD)
Automatically provides:
- list() -> GET /articles/
- create() -> POST /articles/
- retrieve() -> GET /articles/{pk}/
- update() -> PUT /articles/{pk}/
- partial_update() -> PATCH /articles/{pk}/
- destroy() -> DELETE /articles/{pk}/
"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
def perform_create(self, serializer):
# Custom logic during creation
serializer.save(author=self.request.user)
# ViewSet inheritance hierarchy
from rest_framework.viewsets import ViewSet, GenericViewSet
from rest_framework import mixins
# 1. ViewSet (base class - no operations)
class CustomViewSet(ViewSet):
def list(self, request):
return Response({'message': 'List of items'})
def retrieve(self, request, pk=None):
return Response({'message': f'Item {pk}'})
# 2. GenericViewSet (adds queryset and serializer support)
class ArticleViewSet(GenericViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# Still need to add mixins for operations
# 3. GenericViewSet with Mixins (custom operation combinations)
class ArticleViewSet(
mixins.ListModelMixin,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
GenericViewSet
):
"""
List, Create, and Retrieve only (no Update or Delete)
"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 4. ReadOnlyModelViewSet (list + retrieve)
class ArticleViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# Equivalent to: ListModelMixin + RetrieveModelMixin + GenericViewSet
# 5. ModelViewSet (complete CRUD)
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# Equivalent to: All five mixins + GenericViewSet
# Available mixins
from rest_framework import mixins
# mixins.ListModelMixin - list()
# mixins.CreateModelMixin - create()
# mixins.RetrieveModelMixin - retrieve()
# mixins.UpdateModelMixin - update() and partial_update()
# mixins.DestroyModelMixin - destroy()| ViewSet Type | Operations Included | Use Case | Methods |
|---|---|---|---|
| ViewSet | None (manual) | Full custom control | Define all methods manually |
| GenericViewSet | None + queryset support | Custom with mixins | Add mixins as needed |
| ReadOnlyModelViewSet | List, Retrieve | Public read-only APIs | list(), retrieve() |
| ModelViewSet | All CRUD operations | Complete resource management | list(), create(), retrieve(), update(), partial_update(), destroy() |
Automatic URL Routing
Routers automatically generate URL patterns for ViewSets eliminating manual URL configuration and ensuring consistent RESTful URL structures across your API. DRF provides two router types: DefaultRouter adding an API root view with hyperlinks to all registered ViewSets plus format suffixes, and SimpleRouter providing basic URL generation without these extras. Routers scan ViewSet classes discovering standard actions like list, create, retrieve, update, and destroy plus custom actions decorated with action decorator generating appropriate URL patterns following REST conventions. This automatic routing maintains consistency as APIs grow preventing URL pattern errors and reducing maintenance burden. The router configuration typically lives in a dedicated urls.py file within your app organizing API endpoints separately from traditional Django views.
# Basic Router Configuration
from rest_framework.routers import DefaultRouter, SimpleRouter
from django.urls import path, include
from . import views
# Create router instance
router = DefaultRouter()
# Register ViewSets
router.register(r'articles', views.ArticleViewSet, basename='article')
router.register(r'users', views.UserViewSet, basename='user')
router.register(r'categories', views.CategoryViewSet, basename='category')
# URL patterns
urlpatterns = [
path('api/', include(router.urls)),
]
# Generated URLs by DefaultRouter:
# GET /api/articles/ -> ArticleViewSet.list()
# POST /api/articles/ -> ArticleViewSet.create()
# GET /api/articles/{pk}/ -> ArticleViewSet.retrieve()
# PUT /api/articles/{pk}/ -> ArticleViewSet.update()
# PATCH /api/articles/{pk}/ -> ArticleViewSet.partial_update()
# DELETE /api/articles/{pk}/ -> ArticleViewSet.destroy()
# GET /api/ -> API root view (DefaultRouter only)
# DefaultRouter vs SimpleRouter
from rest_framework.routers import DefaultRouter, SimpleRouter
# DefaultRouter - Includes API root view and format suffixes
default_router = DefaultRouter()
default_router.register(r'articles', ArticleViewSet)
# Generates: /api/, /api/articles/, /api/articles/{pk}/
# Also adds: /api/articles.json, /api/articles/{pk}.json (format suffixes)
# SimpleRouter - Basic routing only
simple_router = SimpleRouter()
simple_router.register(r'articles', ArticleViewSet)
# Generates: /api/articles/, /api/articles/{pk}/
# No API root view, no format suffixes
# Multiple routers for API versioning
from django.urls import path, include
v1_router = DefaultRouter()
v1_router.register(r'articles', views.ArticleViewSetV1)
v2_router = DefaultRouter()
v2_router.register(r'articles', views.ArticleViewSetV2)
urlpatterns = [
path('api/v1/', include(v1_router.urls)),
path('api/v2/', include(v2_router.urls)),
]
# Basename parameter
router = DefaultRouter()
# With basename (custom URL names)
router.register(r'articles', ArticleViewSet, basename='article')
# URL names: article-list, article-detail
# Without basename (auto-generated from model)
router.register(r'articles', ArticleViewSet)
# Requires queryset attribute in ViewSet
# Basename auto-generated from model name
# Combining router URLs with custom URLs
from django.urls import path, include
from . import views
router = DefaultRouter()
router.register(r'articles', views.ArticleViewSet)
urlpatterns = [
# Router URLs
path('api/', include(router.urls)),
# Custom URLs alongside router
path('api/statistics/', views.StatisticsView.as_view()),
path('api/search/', views.SearchView.as_view()),
]
# Nested routers (using drf-nested-routers)
# pip install drf-nested-routers
from rest_framework_nested import routers
router = routers.DefaultRouter()
router.register(r'articles', ArticleViewSet, basename='article')
# Nested router for comments under articles
articles_router = routers.NestedDefaultRouter(router, r'articles', lookup='article')
articles_router.register(r'comments', CommentViewSet, basename='article-comments')
urlpatterns = [
path('api/', include(router.urls)),
path('api/', include(articles_router.urls)),
]
# Generated nested URLs:
# GET /api/articles/{article_pk}/comments/
# POST /api/articles/{article_pk}/comments/
# GET /api/articles/{article_pk}/comments/{pk}/Custom Actions and Methods
Custom actions extend ViewSets beyond standard CRUD operations adding endpoints for specific business logic like publishing articles, marking items as favorites, or generating reports. The action decorator marks methods as custom endpoints specifying HTTP methods, URL patterns, and whether the action applies to collections or individual items. Detail actions operate on single objects appearing at URLs like /articles/{pk}/publish/ while list actions operate on collections appearing at /articles/trending/. Custom actions maintain ViewSet cohesion keeping related functionality together rather than scattering it across multiple view classes. These actions integrate with DRF permissions and authentication enabling fine-grained access control per action.
# Custom actions with @action decorator
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# Detail action (operates on single object)
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
"""
Publish an article
URL: POST /api/articles/{pk}/publish/
"""
article = self.get_object()
article.published = True
article.published_date = timezone.now()
article.save()
serializer = self.get_serializer(article)
return Response(serializer.data)
# List action (operates on collection)
@action(detail=False, methods=['get'])
def recent(self, request):
"""
Get recent articles
URL: GET /api/articles/recent/
"""
recent_articles = Article.objects.filter(
published=True
).order_by('-created_at')[:10]
serializer = self.get_serializer(recent_articles, many=True)
return Response(serializer.data)
# Multiple HTTP methods
@action(detail=True, methods=['post', 'delete'])
def like(self, request, pk=None):
"""
Like or unlike an article
URL: POST /api/articles/{pk}/like/
URL: DELETE /api/articles/{pk}/like/
"""
article = self.get_object()
user = request.user
if request.method == 'POST':
article.likes.add(user)
return Response({'status': 'liked'})
elif request.method == 'DELETE':
article.likes.remove(user)
return Response({'status': 'unliked'})
# Custom URL path
@action(detail=False, methods=['get'], url_path='by-category/(?P<category_slug>[^/.]+)')
def by_category(self, request, category_slug=None):
"""
Get articles by category slug
URL: GET /api/articles/by-category/{slug}/
"""
articles = Article.objects.filter(category__slug=category_slug)
serializer = self.get_serializer(articles, many=True)
return Response(serializer.data)
# Action with custom permissions
from rest_framework.permissions import IsAuthenticated, IsAdminUser
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
@action(
detail=True,
methods=['post'],
permission_classes=[IsAdminUser] # Only admins
)
def feature(self, request, pk=None):
"""Feature an article (admin only)"""
article = self.get_object()
article.featured = True
article.save()
return Response({'status': 'featured'})
@action(
detail=False,
methods=['get'],
permission_classes=[IsAuthenticated] # Authenticated users
)
def my_articles(self, request):
"""Get current user's articles"""
articles = Article.objects.filter(author=request.user)
serializer = self.get_serializer(articles, many=True)
return Response(serializer.data)
# Action with custom serializer
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
@action(detail=True, methods=['get'])
def statistics(self, request, pk=None):
"""
Get article statistics with different serializer
"""
article = self.get_object()
# Use custom serializer for statistics
serializer = ArticleStatisticsSerializer(article)
return Response(serializer.data)
def get_serializer_class(self):
"""Return different serializers per action"""
if self.action == 'statistics':
return ArticleStatisticsSerializer
elif self.action == 'list':
return ArticleListSerializer
return ArticleSerializer
# Complex custom actions with validation
from rest_framework import serializers
class BulkActionSerializer(serializers.Serializer):
article_ids = serializers.ListField(
child=serializers.IntegerField(),
required=True
)
action = serializers.ChoiceField(
choices=['publish', 'unpublish', 'delete'],
required=True
)
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
@action(detail=False, methods=['post'])
def bulk_action(self, request):
"""
Perform bulk actions on multiple articles
URL: POST /api/articles/bulk_action/
Body: {"article_ids": [1, 2, 3], "action": "publish"}
"""
serializer = BulkActionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
article_ids = serializer.validated_data['article_ids']
action_type = serializer.validated_data['action']
articles = Article.objects.filter(id__in=article_ids)
if action_type == 'publish':
articles.update(published=True)
elif action_type == 'unpublish':
articles.update(published=False)
elif action_type == 'delete':
articles.delete()
return Response({
'status': 'success',
'affected': len(article_ids)
})Optimizing ViewSet Queries
ViewSets can suffer from N+1 query problems when serializers access related objects without proper query optimization. Overriding get_queryset method enables adding select_related for foreign keys and prefetch_related for many-to-many relationships optimizing database queries. Dynamic query optimization based on action ensures list views don't fetch unnecessary related data while detail views include complete relationships. Understanding Django query optimization techniques remains essential even with ViewSets' higher abstraction level. Proper optimization prevents performance degradation as data grows maintaining fast API response times under load.
# Query optimization in ViewSets
from rest_framework import viewsets
from django.db.models import Count, Prefetch
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
def get_queryset(self):
"""
Optimize queries with select_related and prefetch_related
"""
queryset = Article.objects.all()
# Optimize foreign key relationships (select_related)
queryset = queryset.select_related(
'author', # ForeignKey to User
'category' # ForeignKey to Category
)
# Optimize many-to-many relationships (prefetch_related)
queryset = queryset.prefetch_related(
'tags', # ManyToMany to Tag
'comments' # Reverse ForeignKey from Comment
)
return queryset
# Dynamic optimization based on action
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
def get_queryset(self):
queryset = Article.objects.all()
# List view - minimal data
if self.action == 'list':
queryset = queryset.select_related('author', 'category')
# Detail view - complete data
elif self.action == 'retrieve':
queryset = queryset.select_related(
'author',
'category'
).prefetch_related(
'tags',
'comments__author' # Nested prefetch
)
return queryset
# Filtering and search optimization
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['category', 'published', 'author']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'title']
def get_queryset(self):
queryset = super().get_queryset()
# Always optimize base relationships
queryset = queryset.select_related('author', 'category')
# Add annotations for computed fields
queryset = queryset.annotate(
comment_count=Count('comments')
)
return queryset
# Custom filtering with query parameters
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
def get_queryset(self):
queryset = Article.objects.select_related('author', 'category')
# Filter by query parameters
author_id = self.request.query_params.get('author', None)
if author_id:
queryset = queryset.filter(author_id=author_id)
published = self.request.query_params.get('published', None)
if published == 'true':
queryset = queryset.filter(published=True)
# Filter by date range
date_from = self.request.query_params.get('date_from', None)
if date_from:
queryset = queryset.filter(created_at__gte=date_from)
return queryset
# Optimized prefetch with custom queryset
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
def get_queryset(self):
# Prefetch only approved comments
approved_comments = Comment.objects.filter(approved=True).select_related('author')
queryset = Article.objects.select_related(
'author',
'category'
).prefetch_related(
Prefetch('comments', queryset=approved_comments),
'tags'
)
return queryset| Technique | Use Case | Performance Impact | When to Apply |
|---|---|---|---|
| select_related | Foreign keys, OneToOne | Reduces N queries to 1 | Always for FK in serializer |
| prefetch_related | ManyToMany, reverse FK | Reduces N queries to 2 | When accessing related lists |
| annotate | Computed fields, counts | Moves computation to DB | Aggregations in serializer |
| only/defer | Large model fields | Reduces data transfer | Excluding heavy fields |
| values/values_list | Simple data extraction | Returns dicts, not objects | When objects not needed |
Permissions in ViewSets
ViewSets support fine-grained permission control through permission_classes attribute applying globally or overriding get_permissions method for per-action permissions. Different actions often require different permission levels allowing public read access while restricting write operations to authenticated users or object owners. Custom permissions check object ownership, user roles, or complex business rules ensuring users only access authorized resources. Combining ViewSets with DRF's permission system creates secure APIs enforcing access control at both endpoint and object levels integrated with Django's security features.
# Permission control in ViewSets
from rest_framework import viewsets
from rest_framework.permissions import (
IsAuthenticated,
IsAuthenticatedOrReadOnly,
IsAdminUser,
AllowAny
)
# Global permissions for all actions
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
# GET requests: Anyone
# POST, PUT, PATCH, DELETE: Authenticated users only
# Per-action permissions
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
def get_permissions(self):
"""
Return different permissions based on action
"""
if self.action in ['list', 'retrieve']:
# Anyone can view
permission_classes = [AllowAny]
elif self.action == 'create':
# Authenticated users can create
permission_classes = [IsAuthenticated]
elif self.action in ['update', 'partial_update', 'destroy']:
# Only admins can modify
permission_classes = [IsAdminUser]
else:
# Default
permission_classes = [IsAuthenticated]
return [permission() for permission in permission_classes]
# Custom object-level permissions
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission):
"""
Custom permission: only author can edit
"""
def has_object_permission(self, request, view, obj):
# Read permissions for everyone
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions only for author
return obj.author == request.user
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
# Custom action permissions
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
@action(
detail=True,
methods=['post'],
permission_classes=[IsAdminUser] # Override for this action
)
def publish(self, request, pk=None):
"""Only admins can publish"""
article = self.get_object()
article.published = True
article.save()
return Response({'status': 'published'})
@action(
detail=True,
methods=['post'],
permission_classes=[IsAuthenticated, IsAuthorOrReadOnly] # Different permissions
)
def draft(self, request, pk=None):
"""Only author can save as draft"""
article = self.get_object()
article.published = False
article.save()
return Response({'status': 'draft'})ViewSet Best Practices
- Use ModelViewSet for standard CRUD: ModelViewSet provides complete functionality with minimal code ideal for typical resource APIs
- Optimize queries in get_queryset: Always use select_related and prefetch_related preventing N+1 query problems and maintaining performance
- Implement per-action logic: Override get_serializer_class, get_queryset, and get_permissions for action-specific behavior
- Use custom actions sparingly: Custom actions should represent logical resource operations not arbitrary endpoints maintaining RESTful design
- Choose appropriate router: Use DefaultRouter for development with API root view and SimpleRouter for production when extras aren't needed
- Implement proper permissions: Use get_permissions for action-specific authorization and custom permission classes for object-level control
- Document custom actions: Add docstrings explaining custom action purpose, parameters, and return values aiding API consumers
- Version ViewSets thoughtfully: Create new ViewSet versions for breaking changes maintaining backwards compatibility with existing clients
- Test ViewSet actions: Write comprehensive tests for all ViewSet actions including custom actions, permissions, and edge cases
- Monitor performance: Profile ViewSet queries identifying bottlenecks and optimizing database access patterns as data grows
Conclusion
ViewSets and Routers represent Django REST Framework's highest abstraction level consolidating related views into single classes with automatic URL generation dramatically reducing boilerplate for REST APIs. ModelViewSet provides complete CRUD operations including list, create, retrieve, update, partial_update, and destroy methods requiring only queryset and serializer_class attributes for full functionality. ReadOnlyModelViewSet restricts operations to list and retrieve perfect for public read-only APIs while GenericViewSet with mixins enables custom operation combinations balancing abstraction against flexibility. Routers automatically generate RESTful URL patterns from ViewSets with DefaultRouter adding API root views and format suffixes while SimpleRouter provides basic routing without extras. Custom actions extend ViewSets beyond standard CRUD using action decorator for business logic endpoints like publishing, liking, or bulk operations maintaining resource cohesion. Query optimization through get_queryset with select_related and prefetch_related prevents N+1 problems ensuring performance at scale especially important when serializers access related objects. Permission control integrates through permission_classes and get_permissions enabling per-action authorization from public read access to admin-only modifications with custom permission classes enforcing object-level rules. Following ViewSet best practices including appropriate base class selection, query optimization, per-action customization, sparing custom action use, proper router choice, permission implementation, comprehensive documentation, thoughtful versioning, thorough testing, and performance monitoring ensures building maintainable scalable APIs. Understanding ViewSets and Routers enables rapid API development maintaining code quality as applications grow from prototypes to production systems serving thousands of users across web and mobile platforms throughout Django REST Framework development integrated with core DRF concepts and serialization patterns.
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


