Django REST Framework ViewSets — Core Concepts

What ViewSets replace

Without ViewSets, building a REST API for a single model requires multiple views and URL patterns:

  • GET /articles/ → list all articles (one view)
  • POST /articles/ → create an article (same or separate view)
  • GET /articles/5/ → retrieve article 5 (another view)
  • PUT /articles/5/ → update article 5 (same detail view)
  • DELETE /articles/5/ → delete article 5 (same detail view)

That’s at least two views and two URL patterns per model. For 20 models, that’s 40+ views and URL configurations with repetitive code.

A ViewSet combines all these actions into a single class, and a router generates the URL patterns automatically.

ModelViewSet: the full package

from rest_framework import viewsets, serializers
from myapp.models import Article

class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ['id', 'title', 'content', 'author', 'published_at']

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

These 10 lines give you a complete CRUD API: list, create, retrieve, update, partial update, and delete. ModelViewSet inherits from mixins that implement each action.

Routers generate URLs

from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'articles', ArticleViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
]

This generates:

  • GET/POST /api/articles/
  • GET/PUT/PATCH/DELETE /api/articles/{pk}/

The router also creates a browsable API root that lists all registered endpoints — useful during development.

ViewSet hierarchy

DRF provides several ViewSet classes for different needs:

  • ViewSet — bare base class, no default actions. Write everything from scratch.
  • GenericViewSet — adds queryset and serializer handling but no actions.
  • ModelViewSet — full CRUD: list, create, retrieve, update, destroy.
  • ReadOnlyModelViewSet — only list and retrieve (no write operations).

You can also compose your own by mixing in specific actions:

from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, CreateModelMixin
from rest_framework.viewsets import GenericViewSet

class ArticleViewSet(ListModelMixin, RetrieveModelMixin, CreateModelMixin, GenericViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    # This gives list, retrieve, and create — but not update or delete

This selective composition is powerful when your API intentionally restricts operations.

Custom actions

ViewSets aren’t limited to CRUD. The @action decorator adds custom endpoints:

from rest_framework.decorators import action
from rest_framework.response import Response

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

    @action(detail=True, methods=['post'])
    def publish(self, request, pk=None):
        article = self.get_object()
        article.published_at = timezone.now()
        article.save()
        return Response({'status': 'published'})

    @action(detail=False, methods=['get'])
    def recent(self, request):
        recent = self.queryset.order_by('-created_at')[:10]
        serializer = self.get_serializer(recent, many=True)
        return Response(serializer.data)
  • detail=True creates an endpoint on a specific object: POST /api/articles/5/publish/
  • detail=False creates a collection-level endpoint: GET /api/articles/recent/

Filtering and permissions

ViewSets integrate cleanly with DRF’s filtering and permission systems:

from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ['author', 'published_at']

Permissions apply to all actions by default. Override get_permissions() for per-action control:

def get_permissions(self):
    if self.action in ['update', 'partial_update', 'destroy']:
        return [IsAdminUser()]
    return [IsAuthenticatedOrReadOnly()]

When not to use ViewSets

ViewSets work best for resource-oriented APIs where models map cleanly to endpoints. They’re less suitable when:

  • Your endpoint doesn’t correspond to a single model (aggregations, multi-model operations)
  • The request/response shapes differ significantly from your model structure
  • You need very different logic per HTTP method

In these cases, DRF’s APIView or function-based @api_view give you full control without fighting ViewSet conventions.

Common misconception

Developers sometimes think ViewSets are magic that removes the need to understand REST. ViewSets automate boilerplate but don’t make design decisions for you. You still need to choose the right serializer fields, set proper permissions, handle pagination, and design meaningful URL structures. The automation handles plumbing, not architecture.

The one thing to remember: ViewSets combine related API actions into one class and pair with routers to eliminate URL boilerplate — pick ModelViewSet for full CRUD, compose mixins for selective operations, and fall back to APIView when the convention doesn’t fit.

pythondjangorest-apidrf

See Also