DEV Community

Cover image for Day 71 of #100DaysOfCode — DRF Deeper: ViewSets, Routers, and Token Authentication
M Saad Ahmad
M Saad Ahmad

Posted on

Day 71 of #100DaysOfCode — DRF Deeper: ViewSets, Routers, and Token Authentication

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'),
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)),
]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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'})
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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',
]
Enter fullscreen mode Exit fullscreen mode

2. Run Migrations

python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

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',
    ],
}
Enter fullscreen mode Exit fullscreen mode

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'),
]
Enter fullscreen mode Exit fullscreen mode

Send a POST request to /api/token/ with username and password:

{
    "username": "haris",
    "password": "securepass123"
}
Enter fullscreen mode Exit fullscreen mode

DRF returns:

{
    "token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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."
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
# core/apps.py
from django.apps import AppConfig

class CoreConfig(AppConfig):
    name = 'core'

    def ready(self):
        import core.signals
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)