Django REST API – APIView vs Generic Views vs ViewSets
In this post, we will improve the way we write API views with Django REST Framework.
We will compare APIView, generic views, and ViewSets, and understand how each option reduces repetitive code when building REST APIs.
Post navigation
APIView
Generic Views
ViewSets
Comparison
Final Code
Why do we need different types of views?
In the previous post, we created CRUD endpoints manually using function-based views.
That approach works well for learning, but as the API grows, the code can become repetitive.
List projects
Create projects
Retrieve one project
Update one project
Delete one project
List tasks
Create tasks
Retrieve one task
Update one task
Delete one task
Django REST Framework gives us several ways to write API views, from more manual to more automatic.
APIView → More control, more code
Generic Views → Less repetition
ViewSets → Cleaner CRUD structure
Current function-based views
In the previous post, we used the @api_view decorator to create function-based views.
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import Project
from .serializers import ProjectSerializer
@api_view(['GET', 'POST'])
def project_list(request):
if request.method == 'GET':
projects = Project.objects.all()
serializer = ProjectSerializer(projects, many=True)
return Response(serializer.data)
if request.method == 'POST':
serializer = ProjectSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
This is easy to understand, but we need to manually check the request method and write each operation ourselves.
Now let’s see how we can write the same logic using class-based views.
APIView
Using APIView
APIView is the base class provided by Django REST Framework for class-based API views.
It gives us more structure than function-based views, but we still control each HTTP method manually.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Project
from .serializers import ProjectSerializer
class ProjectListAPIView(APIView):
def get(self, request):
projects = Project.objects.all()
serializer = ProjectSerializer(projects, many=True)
return Response(serializer.data)
def post(self, request):
serializer = ProjectSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
With APIView, each HTTP method becomes a method inside the class.
GET → def get()
POST → def post()
PUT → def put()
PATCH → def patch()
DELETE → def delete()
This makes the code more organized, but it is still quite manual.
APIView detail view
For a detail endpoint, we can create another class.
class ProjectDetailAPIView(APIView):
def get_object(self, pk):
try:
return Project.objects.get(pk=pk)
except Project.DoesNotExist:
return None
def get(self, request, pk):
project = self.get_object(pk)
if project is None:
return Response(
{'error': 'Project not found'},
status=status.HTTP_404_NOT_FOUND
)
serializer = ProjectSerializer(project)
return Response(serializer.data)
def put(self, request, pk):
project = self.get_object(pk)
if project is None:
return Response(
{'error': 'Project not found'},
status=status.HTTP_404_NOT_FOUND
)
serializer = ProjectSerializer(project, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def patch(self, request, pk):
project = self.get_object(pk)
if project is None:
return Response(
{'error': 'Project not found'},
status=status.HTTP_404_NOT_FOUND
)
serializer = ProjectSerializer(project, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk):
project = self.get_object(pk)
if project is None:
return Response(
{'error': 'Project not found'},
status=status.HTTP_404_NOT_FOUND
)
project.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
This gives us full control, but we are still writing a lot of repetitive code.
Connecting APIView to URLs
When using class-based views, we connect them to URLs using .as_view().
from django.urls import path
from .views import ProjectListAPIView, ProjectDetailAPIView
urlpatterns = [
path('projects/', ProjectListAPIView.as_view(), name='project-list'),
path('projects/<int:pk>/', ProjectDetailAPIView.as_view(), name='project-detail'),
]
The final endpoints remain the same.
GET /api/projects/
POST /api/projects/
GET /api/projects/1/
PUT /api/projects/1/
PATCH /api/projects/1/
DELETE /api/projects/1/
Generic Views
Using Generic Views
Generic views reduce the amount of code we need to write.
Instead of manually writing every method, we can use generic classes that already know how to list, create, retrieve, update, and delete resources.
ListAPIView
CreateAPIView
RetrieveAPIView
UpdateAPIView
DestroyAPIView
ListCreateAPIView
RetrieveUpdateDestroyAPIView
For example, we can replace the list and create logic with ListCreateAPIView.
from rest_framework import generics
from .models import Project
from .serializers import ProjectSerializer
class ProjectListCreateAPIView(generics.ListCreateAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
This single class handles both operations.
GET /api/projects/ → List all projects
POST /api/projects/ → Create a new project
Generic detail view
For retrieve, update, partial update, and delete, we can use RetrieveUpdateDestroyAPIView.
class ProjectRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
This replaces all the manual detail logic we wrote with APIView.
GET /api/projects/1/ → Retrieve project with ID 1
PUT /api/projects/1/ → Fully update project with ID 1
PATCH /api/projects/1/ → Partially update project with ID 1
DELETE /api/projects/1/ → Delete project with ID 1
The code becomes much shorter and easier to maintain.
Complete generic views example
Your config/projects/views.py file could now look like this:
from rest_framework import generics
from .models import Project, Task
from .serializers import ProjectSerializer, TaskSerializer
class ProjectListCreateAPIView(generics.ListCreateAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
class ProjectRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
class TaskListCreateAPIView(generics.ListCreateAPIView):
queryset = Task.objects.all()
serializer_class = TaskSerializer
class TaskRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Task.objects.all()
serializer_class = TaskSerializer
With only four classes, we now support CRUD operations for both projects and tasks.
Generic views URLs
The URLs are very similar to the APIView version.
from django.urls import path
from .views import (
ProjectListCreateAPIView,
ProjectRetrieveUpdateDestroyAPIView,
TaskListCreateAPIView,
TaskRetrieveUpdateDestroyAPIView,
)
urlpatterns = [
path('projects/', ProjectListCreateAPIView.as_view(), name='project-list'),
path('projects/<int:pk>/', ProjectRetrieveUpdateDestroyAPIView.as_view(), name='project-detail'),
path('tasks/', TaskListCreateAPIView.as_view(), name='task-list'),
path('tasks/<int:pk>/', TaskRetrieveUpdateDestroyAPIView.as_view(), name='task-detail'),
]
At this point, our API still uses the same endpoints, but the view code is much cleaner.
ViewSets
Using ViewSets
ViewSets take this one step further.
Instead of creating separate views for list and detail endpoints, a ViewSet groups all common actions for a resource in one class.
list → GET /api/projects/
create → POST /api/projects/
retrieve → GET /api/projects/1/
update → PUT /api/projects/1/
partial_update → PATCH /api/projects/1/
destroy → DELETE /api/projects/1/
The most common option for CRUD APIs is ModelViewSet.
from rest_framework import viewsets
from .models import Project, Task
from .serializers import ProjectSerializer, TaskSerializer
class ProjectViewSet(viewsets.ModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
This is much shorter than all previous versions.
Each ModelViewSet automatically provides the common CRUD operations for the selected model and serializer.
ViewSets need routers
Unlike APIView and generic views, ViewSets are usually connected to URLs using routers.
A router automatically creates the URL patterns for the ViewSet.
from rest_framework.routers import DefaultRouter
from .views import ProjectViewSet, TaskViewSet
router = DefaultRouter()
router.register('projects', ProjectViewSet, basename='project')
router.register('tasks', TaskViewSet, basename='task')
urlpatterns = router.urls
The router creates the list and detail routes automatically.
GET /api/projects/
POST /api/projects/
GET /api/projects/1/
PUT /api/projects/1/
PATCH /api/projects/1/
DELETE /api/projects/1/
GET /api/tasks/
POST /api/tasks/
GET /api/tasks/1/
PUT /api/tasks/1/
PATCH /api/tasks/1/
DELETE /api/tasks/1/
The API endpoints stay the same, but the code becomes much more compact.
Comparison
Comparing the three approaches
Each approach has its place.
APIView
- More manual
- More control
- More code
- Good for custom logic
Generic Views
- Less repetitive
- Good for standard CRUD
- Still explicit and easy to understand
ViewSets
- Very compact
- Best for standard REST resources
- Usually used with routers
- Great for larger APIs
For learning, it is useful to understand all three approaches.
For real projects, ViewSets are often a clean choice when the API follows standard CRUD patterns.
Which one should you use?
A practical way to choose is:
Use APIView when:
- You need full control
- The endpoint does not follow normal CRUD patterns
- The logic is very custom
Use Generic Views when:
- You want less code
- You still want separate list and detail views
- You are building common CRUD endpoints
Use ViewSets when:
- You are building standard REST resources
- You want clean and compact code
- You want routers to generate URLs automatically
In this series, we will continue with ViewSets, because they are a good fit for our project and task resources.
Final Code
Final views.py with ViewSets
At the end of this post, our config/projects/views.py file can be simplified to this:
from rest_framework import viewsets
from .models import Project, Task
from .serializers import ProjectSerializer, TaskSerializer
class ProjectViewSet(viewsets.ModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
And our config/projects/urls.py file can be simplified to this:
from rest_framework.routers import DefaultRouter
from .views import ProjectViewSet, TaskViewSet
router = DefaultRouter()
router.register('projects', ProjectViewSet, basename='project')
router.register('tasks', TaskViewSet, basename='task')
urlpatterns = router.urls
With only a few lines of code, we now have complete CRUD endpoints for projects and tasks.
Testing the endpoints
Run the development server:
python manage.py runserver
Then test the API in the browser:
Projects: http://localhost:8000/api/projects/
Tasks: http://localhost:8000/api/tasks/
You can still use the browsable API to create, update, and delete resources.
What we have learned
In this post, we compared three ways of writing views in Django REST Framework.
APIView gives us full control
Generic views reduce repetitive code
ViewSets group CRUD actions in one class
Routers generate URL patterns automatically
ModelViewSet is a clean option for standard CRUD APIs
Our API is now cleaner and easier to maintain.
What comes next?
In the next post, we will focus more deeply on routers and URL structure.
We will see how Django REST Framework generates API routes and how to organize API endpoints properly.
Django REST API: Routers and URL Structure