Yesterday, I got DRF working with APIView and generic views. Today, for day 71, I went deeper: ViewSets and Routers, which are how DRF is actually used in real projects, and then Token Authentication to secure the API. By the end, the API was fully protected and only accessible with a valid token.
The Problem with What We Had
Yesterday's API worked, but had a lot of repetition in the URLs:
path('api/posts/', PostListCreateAPIView.as_view(), name='post-list'),
path('api/posts/<int:pk>/', PostRetrieveUpdateDestroyAPIView.as_view(), name='post-detail'),
Two classes, two URL patterns, for one model. In a real project with ten models, that's twenty classes and twenty URL patterns. ViewSets and Routers solve this.
ViewSets
A ViewSet combines all the related views for a model into a single class. Instead of separate lists and detail views, you write one ViewSet that handles everything.
from rest_framework import viewsets
from .models import Post
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
That's it. ModelViewSet automatically provides:
| Action | HTTP Method | URL |
|---|---|---|
list |
GET | /api/posts/ |
create |
POST | /api/posts/ |
retrieve |
GET | /api/posts/{pk}/ |
update |
PUT | /api/posts/{pk}/ |
partial_update |
PATCH | /api/posts/{pk}/ |
destroy |
DELETE | /api/posts/{pk}/ |
Full CRUD in one class.
Routers
ViewSets need a Router to generate URLs automatically. You don't need to write path() manually anymore.
# core/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import PostViewSet
router = DefaultRouter()
router.register('posts', PostViewSet, basename='post')
urlpatterns = [
path('api/', include(router.urls)),
]
router.register() takes the URL prefix, the ViewSet, and a basename. The Router generates all six URL patterns automatically. One registration replaces two classes and two URL patterns.
Visit http://127.0.0.1:8000/api/. DRF's browsable API now shows you a clickable list of all registered endpoints.
Customizing ViewSets
Filtering the Queryset
class PostViewSet(viewsets.ModelViewSet):
serializer_class = PostSerializer
def get_queryset(self):
return Post.objects.filter(author=self.request.user)
Overriding get_queryset() lets you filter based on the current user, query parameters, or anything else.
Attaching the Author on Create
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
def perform_create(self, serializer):
serializer.save(author=self.request.user)
perform_create() runs when a POST request passes validation. This is where you inject extra data before saving, same concept as commit=False in forms.
Custom Actions
Beyond the standard CRUD actions, you can add your own:
from rest_framework.decorators import action
from rest_framework.response import Response
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
@action(detail=False, methods=['get'])
def published(self, request):
published_posts = Post.objects.filter(is_published=True)
serializer = self.get_serializer(published_posts, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
post = self.get_object()
post.is_published = True
post.save()
return Response({'status': 'post published'})
-
detail=False— action on the list endpoint:/api/posts/published/ -
detail=True— action on a single object:/api/posts/{pk}/publish/
The Router picks these up automatically and generates the URLs.
Read-Only ViewSet
If you only want to expose read operations and not allow creating or editing:
class PostReadOnlyViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Post.objects.filter(is_published=True)
serializer_class = PostSerializer
ReadOnlyModelViewSet only provides list and retrieve. No create, update, or delete.
API Authentication
Right now, the API is completely open. Anyone can hit any endpoint. Authentication fixes that.
Types of Authentication in DRF
DRF supports several authentication methods:
- Session Authentication — uses Django's session framework, works for browser-based clients
- Token Authentication — a token is issued to each user, sent in request headers, works for any client
- JWT Authentication — JSON Web Tokens, more advanced, requires an extra package
- Basic Authentication — username and password in every request, only for development
For APIs consumed by frontend apps or mobile clients, Token Authentication is the standard starting point.
Setting Up Token Authentication
1. Add to INSTALLED_APPS
INSTALLED_APPS = [
...
'rest_framework',
'rest_framework.authtoken',
]
2. Run Migrations
python manage.py migrate
This creates the authtoken_token table in the database.
3. Configure DRF Settings
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
DEFAULT_PERMISSION_CLASSES set to IsAuthenticated means every endpoint now requires authentication by default.
4. Create a Token Obtain Endpoint
# mysite/urls.py
from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [
...
path('api/token/', obtain_auth_token, name='api-token'),
]
Send a POST request to /api/token/ with username and password:
{
"username": "haris",
"password": "securepass123"
}
DRF returns:
{
"token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"
}
Store this token on the client side. Send it in every subsequent request.
Using the Token in Requests
Include the token in the Authorization header of every request:
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
In Postman, add a header with key Authorization and value Token <your_token_here>.
Without the token, the API returns:
{
"detail": "Authentication credentials were not provided."
}
Permissions
Authentication answers "who are you?" Permissions answer "what are you allowed to do?"
Built-in Permission Classes
from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny, IsAuthenticatedOrReadOnly
-
IsAuthenticated— must be logged in -
AllowAny— no restriction, open to everyone -
IsAdminUser— only admin users -
IsAuthenticatedOrReadOnly— anyone can read, only authenticated users can write
Setting Permissions Per ViewSet
You can override the global default on individual views:
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
Now, anyone can list and retrieve posts, but only authenticated users can create, update, or delete.
Custom Permissions
from rest_framework.permissions import BasePermission
class IsAuthorOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in ['GET', 'HEAD', 'OPTIONS']:
return True
return obj.author == request.user
has_object_permission() runs on single-object actions — retrieve, update, delete. Here, it allows anyone to read but only the author to modify.
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
Generating Tokens Automatically on User Registration
Right now, tokens are only created when a user hits /api/token/. It's better to create one automatically when a user registers:
# core/signals.py
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token
from django.dispatch import receiver
@receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
# core/apps.py
from django.apps import AppConfig
class CoreConfig(AppConfig):
name = 'core'
def ready(self):
import core.signals
Now every new user automatically gets a token the moment they register.
The Full API Picture
Client sends request to /api/posts/
↓
DRF checks Authorization header for token
↓
Token valid → identifies the user
↓
Permission classes check if user can perform this action
↓
ViewSet handles the request → queries database
↓
Serializer converts queryset to JSON
↓
Response returned to client
Wrapping Up
ViewSets and Routers cut the API code down dramatically; one class and one registration replaces multiple views and multiple URL patterns. Token Authentication locks the API down so only authenticated clients can access it, and custom permissions make sure users can only modify their own content.
Thanks for reading. Feel free to share your thoughts!
Top comments (0)