Skip to content
Codeloom
Django

DRF ViewSets and Routers: Clean API Endpoints Fast

Build REST APIs with Django REST Framework ViewSets and Routers — ModelViewSet, custom actions, filtering, and URL generation.

·5 min read · By Codeloom
Intermediate 12 min read

What you'll learn

  • What ViewSets are and how they differ from APIView
  • How to use ModelViewSet for full CRUD in minimal code
  • How routers auto-generate URL patterns
  • How to add custom actions with @action
  • How to control which operations are available with mixins

Prerequisites

  • Django models and basic views
  • Django REST Framework installed and configured
  • Basic understanding of serializers

Django REST Framework’s APIView gives you fine-grained control, but you write a lot of boilerplate for standard CRUD operations. ViewSets combine list, create, retrieve, update, and destroy into a single class. Pair them with a Router and you get auto-generated URL patterns. For most APIs, ViewSets cut your code in half without sacrificing flexibility.

APIView vs ViewSet

With APIView, you map HTTP methods to class methods manually:

# Two separate classes for list/create and retrieve/update/delete
class ArticleList(APIView):
    def get(self, request): ...
    def post(self, request): ...

class ArticleDetail(APIView):
    def get(self, request, pk): ...
    def put(self, request, pk): ...
    def delete(self, request, pk): ...

With a ViewSet, one class handles everything:

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

That single class provides list, create, retrieve, update, partial_update, and destroy — six endpoints from four lines of code.

ModelViewSet: Full CRUD

ModelViewSet inherits from GenericViewSet and all five mixins (Create, List, Retrieve, Update, Destroy). Here’s a complete example:

# articles/serializers.py
from rest_framework import serializers
from .models import Article


class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ["id", "title", "slug", "body", "author", "status", "created_at"]
        read_only_fields = ["author", "created_at"]
# articles/views.py
from rest_framework import viewsets, permissions
from .models import Article
from .serializers import ArticleSerializer


class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.select_related("author")
    serializer_class = ArticleSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    lookup_field = "slug"  # Use slug instead of pk in URLs

    def perform_create(self, serializer):
        """Set the author automatically from the request user."""
        serializer.save(author=self.request.user)

Override perform_create, perform_update, and perform_destroy to inject logic without touching the full method.

Routers: Auto-Generated URLs

Routers inspect ViewSets and generate URL patterns automatically:

# articles/urls.py
from rest_framework.routers import DefaultRouter
from .views import ArticleViewSet

router = DefaultRouter()
router.register("articles", ArticleViewSet, basename="article")

urlpatterns = router.urls

This generates:

URL PatternHTTP MethodActionName
/articles/GETlistarticle-list
/articles/POSTcreatearticle-list
/articles/{slug}/GETretrievearticle-detail
/articles/{slug}/PUTupdatearticle-detail
/articles/{slug}/PATCHpartial_updatearticle-detail
/articles/{slug}/DELETEdestroyarticle-detail

Include the router URLs in your project’s root urls.py:

# myproject/urls.py
from django.urls import path, include

urlpatterns = [
    path("api/v1/", include("articles.urls")),
]

SimpleRouter vs DefaultRouter

DefaultRouter adds a root API endpoint that lists all registered routes — useful for browsable API exploration. SimpleRouter skips it. Use DefaultRouter during development and either one in production.

Custom Actions with @action

Standard CRUD isn’t always enough. The @action decorator adds custom endpoints to your ViewSet:

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"], url_path="publish")
    def publish(self, request, slug=None):
        """POST /articles/{slug}/publish/ — Publish a draft article."""
        article = self.get_object()

        if article.status == "published":
            return Response({"error": "Already published"}, status=400)

        article.status = "published"
        article.published_at = timezone.now()
        article.save(update_fields=["status", "published_at"])

        serializer = self.get_serializer(article)
        return Response(serializer.data)

    @action(detail=False, methods=["get"], url_path="my-articles")
    def my_articles(self, request):
        """GET /articles/my-articles/ — List the current user's articles."""
        queryset = self.get_queryset().filter(author=request.user)
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

    @action(detail=True, methods=["get"], url_path="stats")
    def stats(self, request, slug=None):
        """GET /articles/{slug}/stats/ — View count and engagement stats."""
        article = self.get_object()
        return Response({
            "views": article.view_count,
            "likes": article.likes.count(),
            "comments": article.comments.count(),
        })
  • detail=True — the action operates on a single object (/articles/{slug}/publish/)
  • detail=False — the action operates on the collection (/articles/my-articles/)

The router auto-generates URLs for custom actions.

Controlling Available Actions with Mixins

If you don’t want full CRUD, compose your ViewSet from individual mixins:

from rest_framework import viewsets, mixins


class ArticleViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    viewsets.GenericViewSet,
):
    """Read-only API — list and retrieve only."""
    queryset = Article.objects.filter(status="published")
    serializer_class = ArticleSerializer

Available mixins:

  • CreateModelMixin — adds create() (POST)
  • ListModelMixin — adds list() (GET collection)
  • RetrieveModelMixin — adds retrieve() (GET single)
  • UpdateModelMixin — adds update() and partial_update() (PUT/PATCH)
  • DestroyModelMixin — adds destroy() (DELETE)

DRF also provides ReadOnlyModelViewSet as a shortcut for list + retrieve.

Filtering and Searching

Add filtering capabilities using django-filter:

pip install django-filter
# articles/filters.py
import django_filters
from .models import Article


class ArticleFilter(django_filters.FilterSet):
    status = django_filters.CharFilter(field_name="status")
    author = django_filters.NumberFilter(field_name="author__id")
    created_after = django_filters.DateFilter(field_name="created_at", lookup_expr="gte")

    class Meta:
        model = Article
        fields = ["status", "author", "created_after"]
# articles/views.py
from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from .filters import ArticleFilter


class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_class = ArticleFilter
    search_fields = ["title", "body"]
    ordering_fields = ["created_at", "title"]
    ordering = ["-created_at"]  # Default ordering

Now your API supports:

GET /articles/?status=published
GET /articles/?search=django
GET /articles/?ordering=-created_at
GET /articles/?status=published&search=django&ordering=title

Using Different Serializers Per Action

Use get_serializer_class() to return different serializers for list vs detail:

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

    def get_serializer_class(self):
        if self.action == "list":
            return ArticleListSerializer  # Fewer fields, faster
        if self.action == "create":
            return ArticleCreateSerializer
        return ArticleDetailSerializer  # Full detail for retrieve/update

Pagination

Configure pagination globally or per-ViewSet:

# settings.py — global default
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
}
# Per-ViewSet override
from rest_framework.pagination import CursorPagination


class ArticleCursorPagination(CursorPagination):
    page_size = 10
    ordering = "-created_at"


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

Summary

ViewSets eliminate CRUD boilerplate by combining multiple actions into a single class. Pair them with Routers for auto-generated URLs. Use @action for custom endpoints, mixins to control which operations are available, and get_serializer_class() for action-specific serializers. For most REST APIs in Django, ViewSets are the right level of abstraction.