Development Long read api
How do you build APIs with Django?

How do you build APIs with Django?

Serializer validation, ViewSet CRUD, and router-based URL structure.

1 February 2026 21 min read
Share
X in

Introduction

Django REST Framework (DRF) is the industry-standard toolkit for building JSON/XML APIs on Django. Serializers handle data transformation and validation, Views/ViewSets handle HTTP semantics, and permission/throttle layers manage security.

In this article we build a complete blog API from basic Serializer to ModelViewSet. We cover nested serializers, custom validation, and pagination as real-world requirements.

Serializer Fundamentals

A Serializer converts Python objects to JSON and back. is_valid() runs validation; validated_data holds clean input. read_only and write_only fields clarify the API contract.

  • serializer.data: serialized output
  • serializer.save(): create/update hooks
  • to_representation() for custom output format
from rest_framework import serializers

class CommentSerializer(serializers.Serializer):
    email = serializers.EmailField()
    body = serializers.CharField(max_length=1000)

    def validate_body(self, value):
        if len(value.strip()) < 10:
            raise serializers.ValidationError('Comment must be at least 10 characters.')
        return value

ModelSerializer and Relations

ModelSerializer auto-generates fields from Model metadata. PrimaryKeyRelatedField and nested serializers expose ForeignKey/ManyToMany relations. The depth parameter helps prototyping but loses control in production.

class PostSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source='author.username', read_only=True)
    tags = serializers.SlugRelatedField(
        many=True, slug_field='name', queryset=Tag.objects.all()
    )

    class Meta:
        model = Post
        fields = ['id', 'title', 'slug', 'body', 'author_name', 'tags']
        read_only_fields = ['id', 'slug']

APIView and Generic Views

APIView provides separate handlers per HTTP method. Generic views offer patterns like ListCreateAPIView. Remember select_related on querysets for performance.

from rest_framework import generics, permissions

class PostListCreateAPIView(generics.ListCreateAPIView):
    serializer_class = PostSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def get_queryset(self):
        return Post.objects.select_related('author').prefetch_related('tags')
Define queryset in the view; serializer owns shape, view owns access rules.

ViewSet and Router

ViewSet bundles CRUD actions in one class: list, retrieve, create, update, destroy. DefaultRouter generates URLs automatically. The @action decorator adds custom endpoints.

from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.select_related('author')
    serializer_class = PostSerializer
    lookup_field = 'slug'

    @action(detail=True, methods=['post'])
    def publish(self, request, slug=None):
        post = self.get_object()
        post.publish()
        return Response({'status': 'published'})

Permission, Pagination, and Filtering

IsAuthenticated, DjangoModelPermissions, and custom permission classes control access. PageNumberPagination standardizes paging. django-filter adds query param filtering.

  • permission_classes at view or ViewSet level
  • throttle_classes for rate limiting
  • renderer_classes for non-JSON formats
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
    ],
}

Error Handling and Testing

DRF returns consistent error format: {'field': ['message']}. Write integration tests with APIClient. Simulate auth with force_authenticate.

  1. Serializer unit tests: is_valid() and validate_* methods
  2. APITestCase for HTTP status and payload
  3. Factory pattern for test data
from rest_framework.test import APITestCase

class PostAPITest(APITestCase):
    def test_create_requires_auth(self):
        response = self.client.post('/api/posts/', {'title': 'Test'})
        self.assertEqual(response.status_code, 401)

Conclusion

DRF lets you build scalable services without breaking Django's ORM and auth stack. ViewSet + Router speeds CRUD projects; choose APIView or custom actions for complex workflows.

  • Watch N+1 with nested serializers (prefetch_related)
  • Versioning via namespace or URL prefix
  • OpenAPI schema: use drf-spectacular