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.
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 Pattern | HTTP Method | Action | Name |
|---|---|---|---|
/articles/ | GET | list | article-list |
/articles/ | POST | create | article-list |
/articles/{slug}/ | GET | retrieve | article-detail |
/articles/{slug}/ | PUT | update | article-detail |
/articles/{slug}/ | PATCH | partial_update | article-detail |
/articles/{slug}/ | DELETE | destroy | article-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— addscreate()(POST)ListModelMixin— addslist()(GET collection)RetrieveModelMixin— addsretrieve()(GET single)UpdateModelMixin— addsupdate()andpartial_update()(PUT/PATCH)DestroyModelMixin— addsdestroy()(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.
Related articles
- Django Django + Celery: Background Tasks and Task Queues
Integrate Celery with Django for background task processing — setup, writing tasks, retries, periodic tasks, and monitoring with Flower.
- Django Django Custom Managers and Chainable QuerySets
Build custom managers and chainable QuerySets in Django to encapsulate query logic, keep views thin, and write expressive ORM code.
- Django Writing Custom Middleware in Django
Build custom Django middleware — request/response processing, exception handling, middleware ordering, and real-world examples.
- Django Django Multi-Tenancy: Shared Database Schema Patterns
Implement multi-tenancy in Django using shared database schemas — tenant models, middleware, filtered QuerySets, and data isolation.