/// DECRYPTING_CONTENT...SECURE_CONNECTION

Introduction

Django REST Framework (DRF) is a powerful, flexible toolkit for building Web APIs in Python. Built on top of Django, it provides a comprehensive set of tools for serialization, authentication, permissions, throttling, and API browsing. This guide will walk you through building a production-ready REST API from scratch, covering fundamental concepts and best practices.

Understanding REST APIs

REST (Representational State Transfer) is an architectural style for designing networked applications. REST APIs use HTTP requests to perform CRUD (Create, Read, Update, Delete) operations on resources, making them language-agnostic and platform-independent.

Core REST Principles

Resource-Based Architecture

In REST, everything is a resource identified by a URI. Resources are nouns (e.g., /users, /todos) rather than verbs. Each resource can have multiple representations (JSON, XML, HTML).

Statelessness

Each request from client to server must contain all information needed to understand and process the request. The server stores no client context between requests, making the API scalable and reliable.

Standard HTTP Methods

REST APIs leverage HTTP methods with specific semantics:

  • GET: Retrieve a resource or collection (safe, idempotent)
  • POST: Create a new resource (not idempotent)
  • PUT: Update a resource by replacing it entirely (idempotent)
  • PATCH: Partially update a resource (idempotent)
  • DELETE: Remove a resource (idempotent)

HTTP Status Codes

Proper status codes communicate the result of operations:

  • 200 OK: Successful GET, PUT, or PATCH
  • 201 Created: Successful POST
  • 204 No Content: Successful DELETE
  • 400 Bad Request: Invalid request data
  • 401 Unauthorized: Authentication required
  • 403 Forbidden: Authenticated but not authorized
  • 404 Not Found: Resource doesn't exist
  • 500 Internal Server Error: Server-side error

HATEOAS (Hypermedia as the Engine of Application State)

Responses include links to related resources, allowing clients to discover available actions dynamically.

REST API Design Best Practices

Consistent Naming Conventions

Use plural nouns for collections (/todos not /todo), lowercase letters, hyphens for multi-word resources (/user-profiles), and nest resources logically (/users/1/todos).

Versioning

Version your API to manage breaking changes:

/api/v1/todos/
/api/v2/todos/

Filtering, Sorting, and Pagination

Support query parameters for flexible data retrieval:

GET /api/v1/todos/?completed=true&ordering=-created_at&page=2

Meaningful Error Messages

Return structured error responses:

{
  "error": "Validation failed",
  "details": {
    "title": ["This field is required."]
  }
}

Why Django REST Framework?

Django provides an excellent foundation for web applications, and DRF extends it specifically for API development.

Django's Strengths

Robust ORM

Django's Object-Relational Mapper abstracts database operations, supporting PostgreSQL, MySQL, SQLite, and Oracle. The ORM handles migrations, relationships, and complex queries.

Security Features

Django includes protection against SQL injection, XSS, CSRF, clickjacking, and provides secure password hashing out of the box.

Scalability

Django's shared-nothing architecture and caching framework support horizontal scaling.

Admin Interface

The auto-generated admin interface accelerates development by providing immediate CRUD functionality for data management.

Extensive Ecosystem

Django's mature ecosystem includes packages for almost any requirement, from payment processing to full-text search.

DRF's Additional Features

Powerful Serialization

DRF serializers handle complex data type conversions, validation, and nested relationships. They work bidirectionally, serializing Python objects to JSON and deserializing JSON to Python objects.

Class-Based Views and ViewSets

Generic views and viewsets reduce boilerplate code while remaining customizable. DRF provides ready-made classes for common patterns like list-create, retrieve-update-destroy, and pagination.

Authentication and Permissions

Built-in support for session authentication, token authentication, OAuth2, and JWT. Fine-grained permission classes control access at the view and object level.

Browsable API

DRF's browsable API provides an HTML interface for exploring and testing endpoints directly in the browser, improving developer experience significantly.

Content Negotiation

Automatic content negotiation supports multiple response formats (JSON, XML, YAML) based on client Accept headers.

Throttling

Rate limiting prevents API abuse with configurable throttle classes for anonymous and authenticated users.

Filtering and Search

Integration with django-filter and built-in search backends enable sophisticated query capabilities.

Project Setup

Let's build a comprehensive Todo API with authentication, validation, and proper error handling.

Initial Setup

# Create project directory
mkdir django_todo_api
cd django_todo_api

# Create and activate virtual environment
python3 -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install dependencies
pip install django djangorestframework django-filter python-decouple

# Create Django project
django-admin startproject config .

# Create todos app
python manage.py startapp todos

# Create requirements.txt
pip freeze > requirements.txt

Configuration

Configure settings.py:

# config/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    # Third-party apps
    'rest_framework',
    'django_filters',
    
    # Local apps
    'todos',
]

# REST Framework configuration
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
    ],
    'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
}

# Database configuration (using PostgreSQL in production)
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'todo_db',
        'USER': 'postgres',
        'PASSWORD': 'your_password',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

# For development, you can use SQLite
# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.sqlite3',
#         'NAME': BASE_DIR / 'db.sqlite3',
#     }
# }

Building the Models

Create a comprehensive Todo model with all necessary fields:

# todos/models.py

from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MinLengthValidator

class Todo(models.Model):
    PRIORITY_CHOICES = [
        ('low', 'Low'),
        ('medium', 'Medium'),
        ('high', 'High'),
    ]
    
    title = models.CharField(
        max_length=200,
        validators=[MinLengthValidator(3)],
        help_text="Title of the todo item"
    )
    description = models.TextField(
        blank=True,
        help_text="Detailed description of the todo"
    )
    completed = models.BooleanField(
        default=False,
        help_text="Whether the todo is completed"
    )
    priority = models.CharField(
        max_length=10,
        choices=PRIORITY_CHOICES,
        default='medium',
        help_text="Priority level of the todo"
    )
    due_date = models.DateTimeField(
        null=True,
        blank=True,
        help_text="Due date for the todo"
    )
    created_at = models.DateTimeField(
        auto_now_add=True,
        help_text="Timestamp when todo was created"
    )
    updated_at = models.DateTimeField(
        auto_now=True,
        help_text="Timestamp when todo was last updated"
    )
    owner = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='todos',
        help_text="User who created this todo"
    )
    
    class Meta:
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['owner', 'completed']),
            models.Index(fields=['due_date']),
        ]
        verbose_name = 'Todo'
        verbose_name_plural = 'Todos'
    
    def __str__(self):
        return f"{self.title} - {'Completed' if self.completed else 'Pending'}"
    
    def is_overdue(self):
        """Check if todo is past its due date"""
        from django.utils import timezone
        if self.due_date and not self.completed:
            return timezone.now() > self.due_date
        return False

Run Migrations

# Create migration files
python manage.py makemigrations

# Apply migrations
python manage.py migrate

# Create superuser for admin access
python manage.py createsuperuser

Creating Serializers

Serializers convert complex data types (like Django models) to Python datatypes that can be rendered into JSON, XML, or other content types.

# todos/serializers.py

from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Todo
from django.utils import timezone

class UserSerializer(serializers.ModelSerializer):
    """Serializer for User model"""
    todos_count = serializers.SerializerMethodField()
    
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'todos_count']
        read_only_fields = ['id']
    
    def get_todos_count(self, obj):
        return obj.todos.count()

class TodoSerializer(serializers.ModelSerializer):
    """Serializer for Todo model with full details"""
    owner = UserSerializer(read_only=True)
    is_overdue = serializers.SerializerMethodField()
    days_until_due = serializers.SerializerMethodField()
    
    class Meta:
        model = Todo
        fields = [
            'id',
            'title',
            'description',
            'completed',
            'priority',
            'due_date',
            'created_at',
            'updated_at',
            'owner',
            'is_overdue',
            'days_until_due',
        ]
        read_only_fields = ['id', 'created_at', 'updated_at', 'owner']
    
    def get_is_overdue(self, obj):
        """Return whether the todo is overdue"""
        return obj.is_overdue()
    
    def get_days_until_due(self, obj):
        """Calculate days until due date"""
        if obj.due_date and not obj.completed:
            delta = obj.due_date - timezone.now()
            return delta.days
        return None
    
    def validate_title(self, value):
        """Custom validation for title field"""
        if len(value) < 3:
            raise serializers.ValidationError(
                "Title must be at least 3 characters long"
            )
        return value
    
    def validate_due_date(self, value):
        """Ensure due date is not in the past"""
        if value and value < timezone.now():
            raise serializers.ValidationError(
                "Due date cannot be in the past"
            )
        return value
    
    def validate(self, data):
        """Object-level validation"""
        if data.get('completed') and data.get('due_date'):
            # Clear due date when marking as completed
            data['due_date'] = None
        return data

class TodoListSerializer(serializers.ModelSerializer):
    """Lightweight serializer for list views"""
    owner_username = serializers.CharField(source='owner.username', read_only=True)
    
    class Meta:
        model = Todo
        fields = [
            'id',
            'title',
            'completed',
            'priority',
            'due_date',
            'owner_username',
        ]
        read_only_fields = ['id', 'owner_username']

class TodoCreateUpdateSerializer(serializers.ModelSerializer):
    """Serializer for creating and updating todos"""
    
    class Meta:
        model = Todo
        fields = [
            'title',
            'description',
            'completed',
            'priority',
            'due_date',
        ]
    
    def create(self, validated_data):
        """Custom create method to set owner"""
        validated_data['owner'] = self.context['request'].user
        return super().create(validated_data)

Serializer Features Explained

SerializerMethodField

Adds custom computed fields to the serialized output. The method name must be get_<field_name>.

Custom Validation

Field-level validation with validate_<field_name> methods and object-level validation with the validate method catch errors before database operations.

Read-Only Fields

Fields marked as read-only are included in responses but ignored in create/update requests, preventing clients from modifying protected fields.

Source Attribute

The source parameter maps serializer fields to different model attributes or related fields.

Context

The serializer context provides access to the request object, view, and other contextual information.

Building Views

DRF provides several approaches to building views. We'll use ViewSets for clean, DRY code.

# todos/views.py

from rest_framework import viewsets, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from django.utils import timezone

from .models import Todo
from .serializers import (
    TodoSerializer,
    TodoListSerializer,
    TodoCreateUpdateSerializer
)
from .permissions import IsOwnerOrReadOnly
from .filters import TodoFilter

class TodoViewSet(viewsets.ModelViewSet):
    """
    ViewSet for Todo CRUD operations
    
    Provides standard actions:
    - list: GET /todos/
    - create: POST /todos/
    - retrieve: GET /todos/{id}/
    - update: PUT /todos/{id}/
    - partial_update: PATCH /todos/{id}/
    - destroy: DELETE /todos/{id}/
    
    Custom actions:
    - complete: POST /todos/{id}/complete/
    - statistics: GET /todos/statistics/
    """
    queryset = Todo.objects.all()
    permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter
    ]
    filterset_class = TodoFilter
    search_fields = ['title', 'description']
    ordering_fields = ['created_at', 'due_date', 'priority', 'completed']
    ordering = ['-created_at']
    
    def get_queryset(self):
        """Filter queryset to only show user's own todos"""
        return Todo.objects.filter(owner=self.request.user).select_related('owner')
    
    def get_serializer_class(self):
        """Return appropriate serializer based on action"""
        if self.action == 'list':
            return TodoListSerializer
        elif self.action in ['create', 'update', 'partial_update']:
            return TodoCreateUpdateSerializer
        return TodoSerializer
    
    def perform_create(self, serializer):
        """Set owner when creating a new todo"""
        serializer.save(owner=self.request.user)
    
    @action(detail=True, methods=['post'])
    def complete(self, request, pk=None):
        """Mark a todo as completed"""
        todo = self.get_object()
        todo.completed = True
        todo.save()
        
        serializer = self.get_serializer(todo)
        return Response(serializer.data)
    
    @action(detail=True, methods=['post'])
    def uncomplete(self, request, pk=None):
        """Mark a todo as incomplete"""
        todo = self.get_object()
        todo.completed = False
        todo.save()
        
        serializer = self.get_serializer(todo)
        return Response(serializer.data)
    
    @action(detail=False, methods=['get'])
    def statistics(self, request):
        """Get statistics about user's todos"""
        queryset = self.get_queryset()
        
        total = queryset.count()
        completed = queryset.filter(completed=True).count()
        pending = total - completed
        overdue = sum(1 for todo in queryset if todo.is_overdue())
        
        stats = {
            'total': total,
            'completed': completed,
            'pending': pending,
            'overdue': overdue,
            'completion_rate': (completed / total * 100) if total > 0 else 0,
        }
        
        return Response(stats)
    
    @action(detail=False, methods=['post'])
    def bulk_complete(self, request):
        """Mark multiple todos as completed"""
        todo_ids = request.data.get('ids', [])
        
        if not todo_ids:
            return Response(
                {'error': 'No todo IDs provided'},
                status=status.HTTP_400_BAD_REQUEST
            )
        
        queryset = self.get_queryset().filter(id__in=todo_ids)
        updated = queryset.update(completed=True, updated_at=timezone.now())
        
        return Response({
            'message': f'Successfully completed {updated} todos',
            'updated_count': updated
        })

Custom Permissions

Create fine-grained permission control:

# todos/permissions.py

from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners to edit their todos
    """
    
    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed for any request
        if request.method in permissions.SAFE_METHODS:
            return True
        
        # Write permissions only for the owner
        return obj.owner == request.user

Custom Filters

Create sophisticated filtering capabilities:

# todos/filters.py

from django_filters import rest_framework as filters
from .models import Todo

class TodoFilter(filters.FilterSet):
    """Custom filters for Todo model"""
    title = filters.CharFilter(lookup_expr='icontains')
    created_after = filters.DateTimeFilter(field_name='created_at', lookup_expr='gte')
    created_before = filters.DateTimeFilter(field_name='created_at', lookup_expr='lte')
    due_soon = filters.BooleanFilter(method='filter_due_soon')
    overdue = filters.BooleanFilter(method='filter_overdue')
    
    class Meta:
        model = Todo
        fields = ['completed', 'priority', 'owner']
    
    def filter_due_soon(self, queryset, name, value):
        """Filter todos due within the next 7 days"""
        if value:
            from django.utils import timezone
            from datetime import timedelta
            
            now = timezone.now()
            soon = now + timedelta(days=7)
            return queryset.filter(
                due_date__gte=now,
                due_date__lte=soon,
                completed=False
            )
        return queryset
    
    def filter_overdue(self, queryset, name, value):
        """Filter overdue todos"""
        if value:
            from django.utils import timezone
            
            now = timezone.now()
            return queryset.filter(
                due_date__lt=now,
                completed=False
            )
        return queryset

URL Configuration

Set up URL routing with the ViewSet router:

# config/urls.py

from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from todos.views import TodoViewSet

# Create router and register viewsets
router = DefaultRouter()
router.register(r'todos', TodoViewSet, basename='todo')

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include(router.urls)),
    path('api-auth/', include('rest_framework.urls')),  # Browsable API login
]

The DefaultRouter automatically creates these URLs:

  • GET /api/v1/todos/ - List all todos
  • POST /api/v1/todos/ - Create a new todo
  • GET /api/v1/todos/{id}/ - Retrieve a specific todo
  • PUT /api/v1/todos/{id}/ - Update a todo (full update)
  • PATCH /api/v1/todos/{id}/ - Partially update a todo
  • DELETE /api/v1/todos/{id}/ - Delete a todo
  • POST /api/v1/todos/{id}/complete/ - Mark todo as complete
  • POST /api/v1/todos/{id}/uncomplete/ - Mark todo as incomplete
  • GET /api/v1/todos/statistics/ - Get user's todo statistics
  • POST /api/v1/todos/bulk_complete/ - Complete multiple todos

Admin Configuration

Customize the Django admin interface:

# todos/admin.py

from django.contrib import admin
from .models import Todo

@admin.register(Todo)
class TodoAdmin(admin.ModelAdmin):
    list_display = [
        'title',
        'owner',
        'priority',
        'completed',
        'due_date',
        'created_at'
    ]
    list_filter = ['completed', 'priority', 'created_at', 'due_date']
    search_fields = ['title', 'description', 'owner__username']
    readonly_fields = ['created_at', 'updated_at']
    date_hierarchy = 'created_at'
    
    fieldsets = (
        ('Basic Information', {
            'fields': ('title', 'description', 'owner')
        }),
        ('Status', {
            'fields': ('completed', 'priority', 'due_date')
        }),
        ('Timestamps', {
            'fields': ('created_at', 'updated_at'),
            'classes': ('collapse',)
        }),
    )
    
    def get_queryset(self, request):
        """Optimize queries with select_related"""
        queryset = super().get_queryset(request)
        return queryset.select_related('owner')

Testing the API

Using cURL

List all todos:

curl -X GET http://localhost:8000/api/v1/todos/ \
  -H "Authorization: Token your_token_here"

Create a new todo:

curl -X POST http://localhost:8000/api/v1/todos/ \
  -H "Authorization: Token your_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Complete Django tutorial",
    "description": "Finish building the REST API",
    "priority": "high",
    "due_date": "2024-12-31T23:59:59Z"
  }'

Update a todo:

curl -X PATCH http://localhost:8000/api/v1/todos/1/ \
  -H "Authorization: Token your_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "completed": true
  }'

Filter todos:

curl -X GET "http://localhost:8000/api/v1/todos/?completed=false&priority=high&ordering=-due_date" \
  -H "Authorization: Token your_token_here"

Get statistics:

curl -X GET http://localhost:8000/api/v1/todos/statistics/ \
  -H "Authorization: Token your_token_here"

Complete a todo:

curl -X POST http://localhost:8000/api/v1/todos/1/complete/ \
  -H "Authorization: Token your_token_here"

Delete a todo:

curl -X DELETE http://localhost:8000/api/v1/todos/1/ \
  -H "Authorization: Token your_token_here"

Using Python Requests

import requests

BASE_URL = 'http://localhost:8000/api/v1'
TOKEN = 'your_token_here'

headers = {
    'Authorization': f'Token {TOKEN}',
    'Content-Type': 'application/json'
}

# List todos
response = requests.get(f'{BASE_URL}/todos/', headers=headers)
print(response.json())

# Create a todo
data = {
    'title': 'Learn DRF',
    'description': 'Master Django REST Framework',
    'priority': 'high'
}
response = requests.post(f'{BASE_URL}/todos/', json=data, headers=headers)
print(response.json())

# Update a todo
data = {'completed': True}
response = requests.patch(f'{BASE_URL}/todos/1/', json=data, headers=headers)
print(response.json())

Automated Testing

Write comprehensive tests for your API:

# todos/tests.py

from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework import status
from .models import Todo

class TodoAPITestCase(TestCase):
    """Test cases for Todo API endpoints"""
    
    def setUp(self):
        """Set up test client and create test user"""
        self.client = APIClient()
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
        self.client.force_authenticate(user=self.user)
        
        # Create test todo
        self.todo = Todo.objects.create(
            title='Test Todo',
            description='Test Description',
            owner=self.user
        )
    
    def test_list_todos(self):
        """Test retrieving list of todos"""
        response = self.client.get('/api/v1/todos/')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data['results']), 1)
    
    def test_create_todo(self):
        """Test creating a new todo"""
        data = {
            'title': 'New Todo',
            'description': 'New Description',
            'priority': 'high'
        }
        response = self.client.post('/api/v1/todos/', data)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Todo.objects.count(), 2)
        self.assertEqual(response.data['title'], 'New Todo')
    
    def test_create_todo_invalid_data(self):
        """Test creating todo with invalid data"""
        data = {'title': 'AB'}  # Too short
        response = self.client.post('/api/v1/todos/', data)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
    
    def test_retrieve_todo(self):
        """Test retrieving a specific todo"""
        response = self.client.get(f'/api/v1/todos/{self.todo.id}/')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data['title'], 'Test Todo')
    
    def test_update_todo(self):
        """Test updating a todo"""
        data = {'completed': True}
        response = self.client.patch(f'/api/v1/todos/{self.todo.id}/', data)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.todo.refresh_from_db()
        self.assertTrue(self.todo.completed)
    
    def test_delete_todo(self):
        """Test deleting a todo"""
        response = self.client.delete(f'/api/v1/todos/{self.todo.id}/')
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertEqual(Todo.objects.count(), 0)
    
    def test_complete_action(self):
        """Test custom complete action"""
        response = self.client.post(f'/api/v1/todos/{self.todo.id}/complete/')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.todo.refresh_from_db()
        self.assertTrue(self.todo.completed)
    
    def test_statistics(self):
        """Test statistics endpoint"""
        response = self.client.get('/api/v1/todos/statistics/')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertIn('total', response.data)
        self.assertIn('completed', response.data)
    
    def test_unauthorized_access(self):
        """Test that unauthenticated users can't access todos"""
        self.client.force_authenticate(user=None)
        response = self.client.get('/api/v1/todos/')
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
    
    def test_owner_isolation(self):
        """Test that users can only see their own todos"""
        other_user = User.objects.create_user(
            username='otheruser',
            password='otherpass123'
        )
        Todo.objects.create(
            title='Other Todo',
            description='Other Description',
            owner=other_user
        )
        
        response = self.client.get('/api/v1/todos/')
        self.assertEqual(len(response.data['results']), 1)
        self.assertEqual(response.data['results'][0]['title'], 'Test Todo')

# Run tests with:
# python manage.py test

Authentication Implementation

Token Authentication

Enable token authentication:

# Add to INSTALLED_APPS
INSTALLED_APPS = [
    # ...
    'rest_framework.authtoken',
]

# Run migrations
python manage.py migrate

Create authentication views:

# todos/views.py (add these)

from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response

class CustomAuthToken(ObtainAuthToken):
    """Custom token authentication endpoint"""
    
    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(
            data=request.data,
            context={'request': request}
        )
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        token, created = Token.objects.get_or_create(user=user)
        
        return Response({
            'token': token.key,
            'user_id': user.pk,
            'username': user.username,
            'email': user.email
        })

Add to URLs:

# config/urls.py

from todos.views import CustomAuthToken

urlpatterns = [
    # ...
    path('api/v1/auth/login/', CustomAuthToken.as_view(), name='api_token_auth'),
]

JWT Authentication (Alternative)

For JWT authentication, install djangorestframework-simplejwt:

pip install djangorestframework-simplejwt

Configure settings:

# config/settings.py

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': False,
    'BLACKLIST_AFTER_ROTATION': False,
}

Add JWT URLs:

# config/urls.py

from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    # ...
    path('api/v1/auth/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/v1/auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

Advanced Features

Pagination

Custom pagination class:

# todos/pagination.py

from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response

class CustomPagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = 'page_size'
    max_page_size = 100
    
    def get_paginated_response(self, data):
        return Response({
            'links': {
                'next': self.get_next_link(),
                'previous': self.get_previous_link()
            },
            'count': self.page.paginator.count,
            'total_pages': self.page.paginator.num_pages,
            'results': data
        })

Throttling

Configure rate limiting:

# config/settings.py

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '1000/day'
    }
}

Custom throttle class:

# todos/throttling.py

from rest_framework.throttling import UserRateThrottle

class BurstRateThrottle(UserRateThrottle):
    scope = 'burst'
    rate = '60/min'

class SustainedRateThrottle(UserRateThrottle):
    scope = 'sustained'
    rate = '1000/day'

Versioning

Implement API versioning:

# config/settings.py

REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
    'DEFAULT_VERSION': 'v1',
    'ALLOWED_VERSIONS': ['v1', 'v2'],
}
# config/urls.py

urlpatterns = [
    path('api/<str:version>/', include(router.urls)),
]

CORS Configuration

For frontend applications on different domains:

pip install django-cors-headers
# config/settings.py

INSTALLED_APPS = [
    # ...
    'corsheaders',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    # ...
]

# Development settings
CORS_ALLOW_ALL_ORIGINS = True

# Production settings
CORS_ALLOWED_ORIGINS = [
    "https://yourdomain.com",
    "https://www.yourdomain.com",
]

Deployment Considerations

Environment Variables

Use python-decouple for configuration:

# config/settings.py

from decouple import config

SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=lambda v: [s.strip() for s in v.split(',')])

Create .env file:

SECRET_KEY=your-secret-key-here
DEBUG=False
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
DATABASE_URL=postgresql://user:password@localhost/dbname

Production Settings

# config/settings.py

if not DEBUG:
    # Security settings
    SECURE_SSL_REDIRECT = True
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SECURE = True
    SECURE_BROWSER_XSS_FILTER = True
    SECURE_CONTENT_TYPE_NOSNIFF = True
    X_FRAME_OPTIONS = 'DENY'
    
    # HTTPS settings
    SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
    
    # HSTS settings
    SECURE_HSTS_SECONDS = 31536000
    SECURE_HSTS_INCLUDE_SUBDOMAINS = True
    SECURE_HSTS_PRELOAD = True

Static Files

# config/settings.py

STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'

Collect static files:

python manage.py collectstatic

Database Configuration

For PostgreSQL in production:

import dj_database_url

DATABASES = {
    'default': dj_database_url.config(
        default=config('DATABASE_URL'),
        conn_max_age=600,
        conn_health_checks=True,
    )
}

API Documentation

Using drf-spectacular

Install OpenAPI documentation generator:

pip install drf-spectacular

Configure:

# config/settings.py

INSTALLED_APPS = [
    # ...
    'drf_spectacular',
]

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

SPECTACULAR_SETTINGS = {
    'TITLE': 'Todo API',
    'DESCRIPTION': 'A comprehensive Todo management API',
    'VERSION': '1.0.0',
    'SERVE_INCLUDE_SCHEMA': False,
}

Add URLs:

# config/urls.py

from drf_spectacular.views import (
    SpectacularAPIView,
    SpectacularRedocView,
    SpectacularSwaggerView
)

urlpatterns = [
    # ...
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
    path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]

Access documentation at:

Performance Optimization

Database Query Optimization

# Optimize queries with select_related and prefetch_related
queryset = Todo.objects.select_related('owner').prefetch_related('tags')

# Use only() to fetch specific fields
queryset = Todo.objects.only('title', 'completed')

# Use defer() to exclude fields
queryset = Todo.objects.defer('description')

Caching

Configure Redis caching:

pip install django-redis
# config/settings.py

CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        }
    }
}

Use caching in views:

from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator

class TodoViewSet(viewsets.ModelViewSet):
    @method_decorator(cache_page(60 * 15))  # Cache for 15 minutes
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

Summary

In this comprehensive guide, we've covered:

Core Concepts

  • REST API principles and best practices
  • Django and DRF architecture
  • HTTP methods and status codes

Implementation

  • Project setup and configuration
  • Model design with relationships and validation
  • Serializers with custom validation and computed fields
  • ViewSets with custom actions
  • URL routing with routers
  • Custom permissions and filters

Advanced Features

  • Token and JWT authentication
  • Pagination and throttling
  • API versioning
  • CORS configuration
  • Automated testing

Production Readiness

  • Environment configuration
  • Security settings
  • Database optimization
  • Caching strategies
  • API documentation with OpenAPI/Swagger

Django REST Framework provides a robust, flexible foundation for building production-ready APIs. Its comprehensive feature set, excellent documentation, and active community make it an ideal choice for API development in Python.

The framework's design promotes best practices, handles common tasks automatically, and remains flexible for custom requirements. Whether building a simple CRUD API or a complex microservices architecture, DRF provides the tools needed to succeed.

Additional Resources

Official Documentation

Authentication

Testing

Deployment

Best Practices

Happy coding!