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

Example · python
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.

Example · python
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.

Example · python
class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()

    def get_serializer_class(self):
        if self.action == "list":
            return PostListSerializer   # lightweight
        return PostDetailSerializer    # full detail

Filter queryset per action

Restricts the list action to published posts while allowing full access for other actions.

Example · python
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

  • Be the first to comment on this lesson.