Rapid CRUD with Django REST Framework: ModelViewSet + DefaultRouter
Learn how to expose CRUD endpoints in minutes using DRF's ModelViewSet and DefaultRouter, including a full example, routing details, permissions, and common pitfalls to avoid.
20 Aug 2026, 20:26 UTC

Why ModelViewSet + DefaultRouter?
When you need a full set of CRUD operations for a model, Django REST Framework (DRF) can generate the heavy lifting in a single line. A ModelViewSet implements the five standard actions—list, create, retrieve, update, and destroy—based on a queryset and a serializer_class. Coupled with a DefaultRouter, the router automatically creates two clean URL patterns: a collection endpoint (e.g., /books/) and a detail endpoint (e.g., /books/5/). This eliminates boilerplate URL configuration and ensures consistent REST semantics.
Step‑by‑Step Example
1. Define the Model
# books/models.py
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
published = models.DateField()
is_published = models.BooleanField(default=False)
def __str__(self):
return self.title
2. Create the Serializer
# books/serializers.py
from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = '__all__'
3. Build the ViewSet
# books/views.py
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
# Optional: restrict to published books only
def get_queryset(self):
qs = super().get_queryset()
return qs.filter(is_published=True)
# Optional: custom validation on create
def perform_create(self, serializer):
# Example: automatically set title to title case
instance = serializer.save()
instance.title = instance.title.title()
instance.save()
4. Register with DefaultRouter
# books/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import BookViewSet
router = DefaultRouter()
router.register(r'books', BookViewSet, basename='book')
urlpatterns = [
path('', include(router.urls)),
]
5. Wire into the Project
Include the app’s URLs in the project’s urls.py:
# project/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('books.urls')), # API root
]
6. Verify the Endpoints
Run the development server and inspect the API:
# Start the server
python manage.py runserver
# Visit http://127.0.0.1:8000/api/books/ in a browser or via curl
Typical responses:
- GET /api/books/ – returns a list of published books.
- POST /api/books/ – creates a new book (requires JSON body).
- GET /api/books/5/ – retrieves book with primary key 5.
- PUT /api/books/5/ – updates the book.
- DELETE /api/books/5/ – deletes the book.
Run automated tests to ensure correct behavior:
python manage.py test books
To confirm the URL patterns generated by the router, use a third‑party command or inspect manually:
python manage.py show_urls
# Look for:
# /api/books/ [name='book-list']
# /api/books/{pk}/ [name='book-detail']
Common Pitfalls and How to Avoid Them
1. Exposing Sensitive Fields
If queryset is set to Book.objects.all() and the serializer exposes all fields, any user with API access can read private data. Restrict the queryset or adjust the serializer fields:
# In BookViewSet
queryset = Book.objects.filter(is_published=True)
# Or in serializer
class Meta:
fields = ('id', 'title', 'author') # exclude sensitive fields
2. Unsecured Browsable API in Production
DRF’s browsable API is handy during development but can leak model metadata. In production, disable it by setting DEFAULT_RENDERER_CLASSES to JSONRenderer only:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
}
3. Overriding get_queryset Without Care
Dynamic filtering is powerful but can break pagination or ordering if not preserved. Always call super().get_queryset() and apply filters on the returned queryset.
4. Forgetting Permissions
By default, ModelViewSet allows any authenticated user to perform all actions. Define permission_classes to enforce fine‑grained access:
from rest_framework.permissions import IsAuthenticated, IsAdminUser
class BookViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
# For admin‑only delete
def get_permissions(self):
if self.action == 'destroy':
return [IsAdminUser()]
return super().get_permissions()
Limitations of the Quick Path
- Complex Business Rules: If your create or update logic involves cross‑model validation or side effects, a
ModelViewSetmay hide the complexity. In such cases, consider a custom view or a separateAPIView. - Non‑Standard Endpoints:
DefaultRouteronly createslistanddetailroutes. For custom actions you’ll need@actiondecorators or a separate router. - Performance on Large Data: Pagination is enabled by default but you must configure
PAGINATE_BYor useLimitOffsetPaginationfor very large querysets.
Practical Checklist Before Deploying
- Verify that
querysetfilters out objects the user shouldn’t see. - Confirm that the serializer only exposes intended fields.
- Run
python manage.py testto ensure all CRUD actions behave correctly. - Check that the browsable API is disabled or protected in production settings.
- Audit permissions for each action, especially destructive ones.
- Run
python manage.py show_urlsto ensure the expected routes are registered.
Following this pattern gives you a robust, low‑maintenance CRUD API in under ten lines of code while keeping security and extensibility in mind.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.