Simplify CRUD APIs in Django REST Framework with ViewSets and Routers
Learn how Django REST Framework’s ViewSet and Router combo cuts CRUD boilerplate, enforces consistent URLs, and where to add custom actions safely.
14 Oct 2025, 01:58 UTC

Problem: Repetitive CRUD boilerplate
When you create a new model in a Django project, writing separate views for list, retrieve, create, update, and delete quickly becomes tedious. Each endpoint often repeats the same pattern of serializer calls, permission checks, and HTTP response handling, which makes the code harder to maintain and increases the chance of inconsistencies across models.
Thesis: ViewSets + Routers reduce duplication and enforce consistency
Django REST Framework’s ViewSet groups the logic for a set of related actions in a single class. Pairing it with a Router automatically generates the conventional URL patterns for those actions. This approach lets you focus on business logic while the framework handles the repetitive routing and method mapping.
Worked example: Book API with ModelViewSet
Assume you have a simple Book model with fields title, author, and published_date. The steps below show how to expose a full CRUD API with minimal code.
- Define the serializer (
myapp/serializers.py):from rest_framework import serializers from .models import Book class BookSerializer(serializers.ModelSerializer): class Meta: model = Book fields = ['id', 'title', 'author', 'published_date'] - Create a ViewSet (
myapp/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 - Register the ViewSet with a Router (
myapp/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('api/', include(router.urls)), ] - Include the app URLs in the project (
project/urls.py):from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('myapp.urls')), ]
After running the development server (python manage.py runserver), the router creates the following endpoints:
GET /api/books/– list all booksPOST /api/books/– create a new bookGET /api/books/<pk>/– retrieve a single bookPUT /api/books/<pk>/– full updatePATCH /api/books/<pk>/– partial updateDELETE /api/books/<pk>/– delete
Trade‑off/Limitation: Custom actions and URL clarity
The automatic routing works well for standard CRUD, but when you need behavior that does not map to the default actions (e.g., “mark as read” or “bulk import”), you must add a custom method decorated with @action. While this keeps the code inside the ViewSet, the resulting URL can be less obvious at a glance, especially if you nest multiple custom actions or override the router’s trailing_slash behavior. In cases where the API requires deep versioning, complex nesting, or non‑standard HTTP verbs, you may find it clearer to define routes manually with path or re_path.
Actionable closing: Start simple, verify, then extend
- Begin with a Router for each model that only needs standard CRUD. This keeps the URLconf short and ensures consistency.
- Add custom @action methods** only when a specific endpoint cannot be expressed with the default actions. Keep the method name descriptive and document the expected HTTP method in the docstring.
- Verify the generated URLs** before writing tests or consuming the API. Run the following command in the project directory (you need read/write access to the project files):
Alternatively, open a Django shell and inspect the router:python manage.py show_urls
Check that the patterns match the expectations listed above.python manage.py shell >>> from myapp.routers import router >>> [p.pattern for p in router.urls] - Test the endpoints** using the browsable API (
http://localhost:8000/api/books/) or a tool likecurl. Verify that aPOSTreturns201 Created, a successfulGETreturns200 OK, and aDELETEreturns204 No Content.
By relying on ViewSets and Routers for the common case, you reduce boilerplate, keep URL patterns predictable, and reserve manual routing for the exceptions where it truly adds clarity.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.