DRF ViewSets & Routers
Collapse a full CRUD API into one ViewSet class and let a Router wire the URLs automatically.
A ViewSet groups the logic for a resource — list, create, retrieve, update, destroy — into one class. Pair it with a Router and DRF generates all the URL patterns for you. This is the most productive layer of DRF and where most production APIs live.
from rest_framework import viewsets
from rest_framework.routers import DefaultRouter
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
router = DefaultRouter()
router.register('books', BookViewSet)
# GET/POST /books/ and GET/PUT/PATCH/DELETE /books/{pk}/ all exist now.The hooks you'll reach for constantly
get_queryset()— scope rows to the current user or query params (the right place for row-level security).get_serializer_class()— return a different serializer for read vs write, or per action.perform_create()— inject data at save time, e.g.serializer.save(owner=self.request.user).@action— add custom endpoints like/books/{pk}/publish/beyond the standard CRUD.
Example
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.utils import timezone
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleReadSerializer
def get_queryset(self):
# Row-level security: authors see their own drafts + everyone's published.
user = self.request.user
base = Article.objects.select_related('author').prefetch_related('tags')
if user.is_staff:
return base
return base.filter(models.Q(status='published') | models.Q(author=user))
def get_serializer_class(self):
# Rich nested serializer for reads, flat one for writes.
if self.action in {'create', 'update', 'partial_update'}:
return ArticleWriteSerializer
return ArticleReadSerializer
def perform_create(self, serializer):
# The client can NEVER spoof the author — we set it from the request.
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
article = self.get_object()
if article.author != request.user and not request.user.is_staff:
return Response(status=status.HTTP_403_FORBIDDEN)
article.status = 'published'
article.published_at = timezone.now()
article.save(update_fields=['status', 'published_at'])
return Response(ArticleReadSerializer(article).data)
@action(detail=False)
def mine(self, request):
page = self.paginate_queryset(self.get_queryset().filter(author=request.user))
return self.get_paginated_response(self.get_serializer(page, many=True).data)
# urls.py
# router = DefaultRouter()
# router.register('articles', ArticleViewSet, basename='article')
# urlpatterns = [path('api/', include(router.urls))]When to use it
- A developer uses a SimpleRouter to auto-generate list and detail URLs for a ViewSet without writing urlpatterns.
- A team nests routers with drf-nested-routers to create URLs like /authors/{id}/posts/ for related resources.
- A developer overrides get_serializer_class() in a ViewSet to use a lightweight serializer for list and a full one for detail.
More examples
DefaultRouter with ViewSet
Registers a ViewSet with DefaultRouter, which generates list, detail, create, update, and delete URLs.
from rest_framework.routers import DefaultRouter
from .views import PostViewSet
router = DefaultRouter()
router.register(r"posts", PostViewSet, basename="post")
# In urls.py:
# urlpatterns = [path("api/", include(router.urls))]Different serializers per action
Returns a compact serializer for the list action and a richer one for retrieve/create/update.
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
def get_serializer_class(self):
if self.action == "list":
return PostListSerializer # lightweight
return PostDetailSerializer # full detailFilter queryset per action
Restricts the list action to published posts while allowing full access for other actions.
class PostViewSet(viewsets.ModelViewSet):
serializer_class = PostSerializer
def get_queryset(self):
qs = Post.objects.all()
if self.action == "list":
return qs.filter(draft=False)
return qs # include drafts for detail/edit
Discussion