Django REST API Tutorial

    Table of Contents

    1. Introduction
    2. Prerequisites
    3. Environment Setup
    4. Creating a Django Project
    5. Installing Django REST Framework
    6. Building Your First API
    7. Models and Serializers
    8. ViewSets and Routers
    9. Authentication and Permissions
    10. Filtering, Searching, and Pagination
    11. Testing Your API
    12. Advanced Features
    13. Deployment
    14. Best Practices

    Introduction

    Django REST Framework (DRF) is a powerful toolkit for building Web APIs in Django. It provides:

    • Serialization for both ORM and non-ORM data sources
    • Authentication policies including OAuth1 and OAuth2
    • Browsable API for ease of development
    • Extensive documentation

    What We’ll Build

    In this tutorial, we’ll build a complete Book Management API with:

    • CRUD operations for books and authors
    • User authentication
    • Permissions and authorization
    • Search and filtering
    • Rate limiting
    • API documentation

    Prerequisites

    Before starting, you should have:

    • Python 3.8 or higher installed
    • Basic knowledge of Python
    • Understanding of HTTP methods (GET, POST, PUT, DELETE)
    • Familiarity with Django basics (recommended)
    • pip package manager

    Environment Setup

    1. Create a Project Directory

    mkdir django_book_api
    cd django_book_api
    Bash

    2. Create a Virtual Environment

    # Windows
    python -m venv venv
    venv\Scripts\activate
    
    # macOS/Linux
    python3 -m venv venv
    source venv/bin/activate
    Bash

    3. Install Required Packages

    pip install django djangorestframework
    pip install django-filter markdown
    pip install djangorestframework-simplejwt
    pip install django-cors-headers
    Bash

    4. Create requirements.txt

    pip freeze > requirements.txt
    Bash

    Creating a Django Project

    1. Start a New Django Project

    django-admin startproject bookapi .
    Bash

    2. Create an App

    python manage.py startapp books
    Bash

    3. Project Structure

    django_book_api/
    ├── bookapi/
       ├── __init__.py
       ├── settings.py
       ├── urls.py
       ├── asgi.py
       └── wsgi.py
    ├── books/
       ├── migrations/
       ├── __init__.py
       ├── admin.py
       ├── apps.py
       ├── models.py
       ├── tests.py
       └── views.py
    ├── manage.py
    └── requirements.txt
    Bash

    Installing Django REST Framework

    1. Update settings.py

    # bookapi/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',
        'rest_framework.authtoken',
        'django_filters',
        'corsheaders',
    
        # Local apps
        'books.apps.BooksConfig',
    ]
    
    MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'corsheaders.middleware.CorsMiddleware',  # CORS
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ]
    
    # REST Framework Configuration
    REST_FRAMEWORK = {
        'DEFAULT_PERMISSION_CLASSES': [
            'rest_framework.permissions.IsAuthenticatedOrReadOnly',
        ],
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework.authentication.SessionAuthentication',
            'rest_framework.authentication.TokenAuthentication',
        ],
        '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',
        ],
    }
    
    # CORS Settings
    CORS_ALLOWED_ORIGINS = [
        "http://localhost:3000",
        "http://127.0.0.1:3000",
    ]
    Python

    2. Run Initial Migrations

    python manage.py migrate
    Python

    Building Your First API

    1. Create a Simple Model

    # books/models.py
    
    from django.db import models
    from django.contrib.auth.models import User
    
    class Author(models.Model):
        name = models.CharField(max_length=200)
        email = models.EmailField(unique=True)
        bio = models.TextField(blank=True)
        created_at = models.DateTimeField(auto_now_add=True)
    
        def __str__(self):
            return self.name
    
        class Meta:
            ordering = ['name']
    
    
    class Book(models.Model):
        GENRE_CHOICES = [
            ('fiction', 'Fiction'),
            ('nonfiction', 'Non-Fiction'),
            ('mystery', 'Mystery'),
            ('scifi', 'Science Fiction'),
            ('fantasy', 'Fantasy'),
            ('biography', 'Biography'),
        ]
    
        title = models.CharField(max_length=200)
        author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
        isbn = models.CharField(max_length=13, unique=True)
        genre = models.CharField(max_length=20, choices=GENRE_CHOICES)
        publication_date = models.DateField()
        pages = models.IntegerField()
        description = models.TextField()
        price = models.DecimalField(max_digits=6, decimal_places=2)
        in_stock = models.BooleanField(default=True)
        created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
        created_at = models.DateTimeField(auto_now_add=True)
        updated_at = models.DateTimeField(auto_now=True)
    
        def __str__(self):
            return self.title
    
        class Meta:
            ordering = ['-created_at']
            indexes = [
                models.Index(fields=['title']),
                models.Index(fields=['isbn']),
            ]
    Python

    2. Create and Run Migrations

    python manage.py makemigrations
    python manage.py migrate
    Bash

    3. Register Models in Admin

    # books/admin.py
    
    from django.contrib import admin
    from .models import Author, Book
    
    @admin.register(Author)
    class AuthorAdmin(admin.ModelAdmin):
        list_display = ['name', 'email', 'created_at']
        search_fields = ['name', 'email']
        list_filter = ['created_at']
    
    @admin.register(Book)
    class BookAdmin(admin.ModelAdmin):
        list_display = ['title', 'author', 'isbn', 'genre', 'price', 'in_stock']
        list_filter = ['genre', 'in_stock', 'publication_date']
        search_fields = ['title', 'isbn', 'author__name']
        date_hierarchy = 'publication_date'
    Python

    4. Create a Superuser

    python manage.py createsuperuser
    Bash

    Models and Serializers

    1. Create Serializers

    # books/serializers.py
    
    from rest_framework import serializers
    from .models import Author, Book
    from django.contrib.auth.models import User
    
    
    class AuthorSerializer(serializers.ModelSerializer):
        books_count = serializers.SerializerMethodField()
    
        class Meta:
            model = Author
            fields = ['id', 'name', 'email', 'bio', 'books_count', 'created_at']
            read_only_fields = ['created_at']
    
        def get_books_count(self, obj):
            return obj.books.count()
    
        def validate_email(self, value):
            if 'test' in value.lower():
                raise serializers.ValidationError("Test emails are not allowed")
            return value
    
    
    class BookListSerializer(serializers.ModelSerializer):
        """Lightweight serializer for list views"""
        author_name = serializers.CharField(source='author.name', read_only=True)
    
        class Meta:
            model = Book
            fields = ['id', 'title', 'author_name', 'isbn', 'genre', 'price', 'in_stock']
    
    
    class BookDetailSerializer(serializers.ModelSerializer):
        """Detailed serializer for single book view"""
        author = AuthorSerializer(read_only=True)
        author_id = serializers.PrimaryKeyRelatedField(
            queryset=Author.objects.all(),
            source='author',
            write_only=True
        )
        created_by_username = serializers.CharField(source='created_by.username', read_only=True)
    
        class Meta:
            model = Book
            fields = [
                'id', 'title', 'author', 'author_id', 'isbn', 'genre',
                'publication_date', 'pages', 'description', 'price',
                'in_stock', 'created_by_username', 'created_at', 'updated_at'
            ]
            read_only_fields = ['created_at', 'updated_at', 'created_by']
    
        def validate_isbn(self, value):
            if len(value) not in [10, 13]:
                raise serializers.ValidationError("ISBN must be 10 or 13 characters")
            return value
    
        def validate_pages(self, value):
            if value <= 0:
                raise serializers.ValidationError("Pages must be greater than 0")
            return value
    
        def validate(self, data):
            if data.get('price', 0) < 0:
                raise serializers.ValidationError("Price cannot be negative")
            return data
    
    
    class UserSerializer(serializers.ModelSerializer):
        class Meta:
            model = User
            fields = ['id', 'username', 'email', 'first_name', 'last_name']
    Python

    2. Understanding Serializers

    Key Concepts:

    • serializers.ModelSerializer: Automatically generates fields based on model
    • read_only_fields: Fields that can’t be modified
    • SerializerMethodField: Custom computed fields
    • validate_: Field-level validation
    • validate(): Object-level validation

    ViewSets and Routers

    1. Create Views

    # books/views.py
    
    from rest_framework import viewsets, filters, status
    from rest_framework.decorators import action
    from rest_framework.response import Response
    from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
    from django_filters.rest_framework import DjangoFilterBackend
    from .models import Author, Book
    from .serializers import (
        AuthorSerializer, BookListSerializer, 
        BookDetailSerializer, UserSerializer
    )
    from .permissions import IsOwnerOrReadOnly
    
    
    class AuthorViewSet(viewsets.ModelViewSet):
        """
        ViewSet for Author model
        Provides CRUD operations for authors
        """
        queryset = Author.objects.all()
        serializer_class = AuthorSerializer
        permission_classes = [IsAuthenticatedOrReadOnly]
        filter_backends = [filters.SearchFilter, filters.OrderingFilter]
        search_fields = ['name', 'email', 'bio']
        ordering_fields = ['name', 'created_at']
    
        @action(detail=True, methods=['get'])
        def books(self, request, pk=None):
            """Get all books by this author"""
            author = self.get_object()
            books = author.books.all()
            serializer = BookListSerializer(books, many=True)
            return Response(serializer.data)
    
        @action(detail=False, methods=['get'])
        def top_authors(self, request):
            """Get authors with most books"""
            from django.db.models import Count
            authors = Author.objects.annotate(
                book_count=Count('books')
            ).order_by('-book_count')[:5]
            serializer = self.get_serializer(authors, many=True)
            return Response(serializer.data)
    
    
    class BookViewSet(viewsets.ModelViewSet):
        """
        ViewSet for Book model
        Provides CRUD operations for books with filtering and search
        """
        queryset = Book.objects.select_related('author', 'created_by').all()
        permission_classes = [IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
        filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
        filterset_fields = ['genre', 'in_stock', 'author']
        search_fields = ['title', 'description', 'author__name']
        ordering_fields = ['title', 'price', 'publication_date', 'created_at']
    
        def get_serializer_class(self):
            """Use different serializers for list and detail views"""
            if self.action == 'list':
                return BookListSerializer
            return BookDetailSerializer
    
        def perform_create(self, serializer):
            """Set the created_by field to current user"""
            serializer.save(created_by=self.request.user)
    
        @action(detail=True, methods=['post'])
        def toggle_stock(self, request, pk=None):
            """Toggle book stock status"""
            book = self.get_object()
            book.in_stock = not book.in_stock
            book.save()
            serializer = self.get_serializer(book)
            return Response(serializer.data)
    
        @action(detail=False, methods=['get'])
        def available(self, request):
            """Get all books in stock"""
            books = self.queryset.filter(in_stock=True)
            serializer = BookListSerializer(books, many=True)
            return Response(serializer.data)
    
        @action(detail=False, methods=['get'])
        def by_genre(self, request):
            """Get books grouped by genre"""
            from django.db.models import Count
            genre_stats = Book.objects.values('genre').annotate(
                count=Count('id')
            ).order_by('-count')
            return Response(genre_stats)
    Python

    2. Create Custom Permissions

    # books/permissions.py
    
    from rest_framework import permissions
    
    
    class IsOwnerOrReadOnly(permissions.BasePermission):
        """
        Custom permission to only allow owners of an object to edit it.
        """
    
        def has_object_permission(self, request, view, obj):
            # Read permissions are allowed to any request
            if request.method in permissions.SAFE_METHODS:
                return True
    
            # Write permissions are only allowed to the owner
            return obj.created_by == request.user
    
    
    class IsAdminOrReadOnly(permissions.BasePermission):
        """
        Custom permission to only allow admin users to edit.
        """
    
        def has_permission(self, request, view):
            if request.method in permissions.SAFE_METHODS:
                return True
            return request.user and request.user.is_staff
    Python

    3. Configure URLs with Routers

    # books/urls.py
    
    from django.urls import path, include
    from rest_framework.routers import DefaultRouter
    from .views import AuthorViewSet, BookViewSet
    
    router = DefaultRouter()
    router.register(r'authors', AuthorViewSet, basename='author')
    router.register(r'books', BookViewSet, basename='book')
    
    urlpatterns = [
        path('', include(router.urls)),
    ]
    Python

    4. Update Project URLs

    # bookapi/urls.py
    
    from django.contrib import admin
    from django.urls import path, include
    from rest_framework.authtoken.views import obtain_auth_token
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('api/', include('books.urls')),
        path('api-auth/', include('rest_framework.urls')),
        path('api/token/', obtain_auth_token, name='api_token_auth'),
    ]
    Python

    Authentication and Permissions

    1. Token Authentication Setup

    Create tokens for users:

    # Create a management command: books/management/commands/create_tokens.py
    
    from django.core.management.base import BaseCommand
    from django.contrib.auth.models import User
    from rest_framework.authtoken.models import Token
    
    
    class Command(BaseCommand):
        help = 'Create tokens for all users'
    
        def handle(self, *args, **kwargs):
            for user in User.objects.all():
                Token.objects.get_or_create(user=user)
            self.stdout.write(self.style.SUCCESS('Tokens created successfully'))
    Python

    Run it:

    python manage.py create_tokens
    Bash

    2. JWT Authentication (Alternative)

    Install package:

    pip install djangorestframework-simplejwt
    Bash

    Update settings:

    # bookapi/settings.py
    
    from datetime import timedelta
    
    REST_FRAMEWORK = {
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework_simplejwt.authentication.JWTAuthentication',
        ],
    }
    
    SIMPLE_JWT = {
        'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
        'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
        'ROTATE_REFRESH_TOKENS': False,
        'BLACKLIST_AFTER_ROTATION': True,
    }
    Python

    Add JWT URLs:

    # bookapi/urls.py
    
    from rest_framework_simplejwt.views import (
        TokenObtainPairView,
        TokenRefreshView,
    )
    
    urlpatterns = [
        # ... other urls
        path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
        path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    ]
    Python

    3. Custom User Registration

    # books/views.py (add this)
    
    from rest_framework import generics
    from rest_framework.permissions import AllowAny
    from django.contrib.auth.models import User
    from rest_framework.response import Response
    from rest_framework import status
    
    
    class UserRegistrationView(generics.CreateAPIView):
        queryset = User.objects.all()
        permission_classes = [AllowAny]
    
        def post(self, request, *args, **kwargs):
            username = request.data.get('username')
            password = request.data.get('password')
            email = request.data.get('email')
    
            if not username or not password:
                return Response(
                    {'error': 'Username and password are required'},
                    status=status.HTTP_400_BAD_REQUEST
                )
    
            if User.objects.filter(username=username).exists():
                return Response(
                    {'error': 'Username already exists'},
                    status=status.HTTP_400_BAD_REQUEST
                )
    
            user = User.objects.create_user(
                username=username,
                password=password,
                email=email
            )
    
            return Response(
                UserSerializer(user).data,
                status=status.HTTP_201_CREATED
            )
    Python

    Add to URLs:

    # books/urls.py
    
    from .views import UserRegistrationView
    
    urlpatterns = [
        path('', include(router.urls)),
        path('register/', UserRegistrationView.as_view(), name='register'),
    ]
    Python

    4. Permission Classes Overview

    # Common permission classes:
    
    # 1. AllowAny - No restrictions
    from rest_framework.permissions import AllowAny
    
    # 2. IsAuthenticated - Must be logged in
    from rest_framework.permissions import IsAuthenticated
    
    # 3. IsAuthenticatedOrReadOnly - Read for all, write for authenticated
    from rest_framework.permissions import IsAuthenticatedOrReadOnly
    
    # 4. IsAdminUser - Only admin users
    from rest_framework.permissions import IsAdminUser
    Python

    Filtering, Searching, and Pagination

    1. Advanced Filtering

    # books/filters.py
    
    from django_filters import rest_framework as filters
    from .models import Book
    
    
    class BookFilter(filters.FilterSet):
        title = filters.CharFilter(lookup_expr='icontains')
        min_price = filters.NumberFilter(field_name='price', lookup_expr='gte')
        max_price = filters.NumberFilter(field_name='price', lookup_expr='lte')
        min_pages = filters.NumberFilter(field_name='pages', lookup_expr='gte')
        max_pages = filters.NumberFilter(field_name='pages', lookup_expr='lte')
        published_after = filters.DateFilter(field_name='publication_date', lookup_expr='gte')
        published_before = filters.DateFilter(field_name='publication_date', lookup_expr='lte')
    
        class Meta:
            model = Book
            fields = ['genre', 'in_stock', 'author']
    
    
    # Update BookViewSet
    class BookViewSet(viewsets.ModelViewSet):
        # ... existing code
        filterset_class = BookFilter  # Use this instead of filterset_fields
    Python

    2. Custom Pagination

    # books/pagination.py
    
    from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination
    
    
    class BookPagination(PageNumberPagination):
        page_size = 10
        page_size_query_param = 'page_size'
        max_page_size = 100
    
        def get_paginated_response(self, data):
            return Response({
                'count': self.page.paginator.count,
                'next': self.get_next_link(),
                'previous': self.get_previous_link(),
                'total_pages': self.page.paginator.num_pages,
                'current_page': self.page.number,
                'results': data
            })
    
    
    class CustomLimitOffsetPagination(LimitOffsetPagination):
        default_limit = 10
        max_limit = 100
    Python

    Apply to ViewSet:

    # books/views.py
    
    from .pagination import BookPagination
    
    class BookViewSet(viewsets.ModelViewSet):
        # ... existing code
        pagination_class = BookPagination
    Python

    3. Query Examples

    # Search
    GET /api/books/?search=harry
    
    # Filter by genre
    GET /api/books/?genre=fiction
    
    # Filter by multiple fields
    GET /api/books/?genre=fiction&in_stock=true
    
    # Price range
    GET /api/books/?min_price=10&max_price=50
    
    # Date range
    GET /api/books/?published_after=2020-01-01&published_before=2023-12-31
    
    # Ordering
    GET /api/books/?ordering=-price  # Descending
    GET /api/books/?ordering=title   # Ascending
    
    # Pagination
    GET /api/books/?page=2
    GET /api/books/?page=2&page_size=20
    
    # Combined
    GET /api/books/?genre=fiction&search=harry&ordering=-price&page=1
    ANSI

    Testing Your API

    1. Unit Tests

    # books/tests.py
    
    from django.test import TestCase
    from django.contrib.auth.models import User
    from rest_framework.test import APITestCase, APIClient
    from rest_framework import status
    from .models import Author, Book
    from datetime import date
    
    
    class AuthorModelTest(TestCase):
        def setUp(self):
            self.author = Author.objects.create(
                name="J.K. Rowling",
                email="jk@example.com",
                bio="British author"
            )
    
        def test_author_creation(self):
            self.assertEqual(self.author.name, "J.K. Rowling")
            self.assertEqual(str(self.author), "J.K. Rowling")
    
        def test_author_books_count(self):
            Book.objects.create(
                title="Harry Potter",
                author=self.author,
                isbn="1234567890",
                genre="fantasy",
                publication_date=date(1997, 6, 26),
                pages=223,
                description="A wizard's tale",
                price=29.99
            )
            self.assertEqual(self.author.books.count(), 1)
    
    
    class BookAPITest(APITestCase):
        def setUp(self):
            # Create user
            self.user = User.objects.create_user(
                username='testuser',
                password='testpass123'
            )
    
            # Create author
            self.author = Author.objects.create(
                name="Test Author",
                email="author@test.com"
            )
    
            # Create book
            self.book = Book.objects.create(
                title="Test Book",
                author=self.author,
                isbn="1234567890",
                genre="fiction",
                publication_date=date(2020, 1, 1),
                pages=200,
                description="A test book",
                price=19.99,
                created_by=self.user
            )
    
            self.client = APIClient()
    
        def test_get_books_list(self):
            """Test retrieving list of books"""
            response = self.client.get('/api/books/')
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.assertEqual(len(response.data['results']), 1)
    
        def test_get_book_detail(self):
            """Test retrieving a single book"""
            response = self.client.get(f'/api/books/{self.book.id}/')
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.assertEqual(response.data['title'], 'Test Book')
    
        def test_create_book_authenticated(self):
            """Test creating a book while authenticated"""
            self.client.force_authenticate(user=self.user)
            data = {
                'title': 'New Book',
                'author_id': self.author.id,
                'isbn': '9876543210',
                'genre': 'mystery',
                'publication_date': '2021-01-01',
                'pages': 300,
                'description': 'A new book',
                'price': '24.99'
            }
            response = self.client.post('/api/books/', data)
            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
            self.assertEqual(Book.objects.count(), 2)
    
        def test_create_book_unauthenticated(self):
            """Test creating a book without authentication"""
            data = {
                'title': 'New Book',
                'author_id': self.author.id,
                'isbn': '9876543210',
                'genre': 'mystery',
                'publication_date': '2021-01-01',
                'pages': 300,
                'description': 'A new book',
                'price': '24.99'
            }
            response = self.client.post('/api/books/', data)
            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
    
        def test_update_book_owner(self):
            """Test updating a book as owner"""
            self.client.force_authenticate(user=self.user)
            data = {'title': 'Updated Title'}
            response = self.client.patch(f'/api/books/{self.book.id}/', data)
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.book.refresh_from_db()
            self.assertEqual(self.book.title, 'Updated Title')
    
        def test_delete_book(self):
            """Test deleting a book"""
            self.client.force_authenticate(user=self.user)
            response = self.client.delete(f'/api/books/{self.book.id}/')
            self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
            self.assertEqual(Book.objects.count(), 0)
    
        def test_filter_books_by_genre(self):
            """Test filtering books by genre"""
            response = self.client.get('/api/books/?genre=fiction')
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.assertEqual(len(response.data['results']), 1)
    
        def test_search_books(self):
            """Test searching books"""
            response = self.client.get('/api/books/?search=Test')
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.assertEqual(len(response.data['results']), 1)
    Python

    2. Run Tests

    # Run all tests
    python manage.py test
    
    # Run specific test class
    python manage.py test books.tests.BookAPITest
    
    # Run specific test method
    python manage.py test books.tests.BookAPITest.test_get_books_list
    
    # Run with verbosity
    python manage.py test --verbosity=2
    
    # Keep test database
    python manage.py test --keepdb
    Bash

    3. Coverage Report

    pip install coverage
    
    # Run tests with coverage
    coverage run --source='.' manage.py test
    
    # Generate report
    coverage report
    
    # Generate HTML report
    coverage html
    Bash

    Advanced Features

    1. Throttling (Rate Limiting)

    # bookapi/settings.py
    
    REST_FRAMEWORK = {
        # ... existing settings
        'DEFAULT_THROTTLE_CLASSES': [
            'rest_framework.throttling.AnonRateThrottle',
            'rest_framework.throttling.UserRateThrottle'
        ],
        'DEFAULT_THROTTLE_RATES': {
            'anon': '100/day',
            'user': '1000/day'
        }
    }
    Python

    Custom Throttle:

    # books/throttles.py
    
    from rest_framework.throttling import UserRateThrottle
    
    
    class BurstRateThrottle(UserRateThrottle):
        scope = 'burst'
        rate = '60/min'
    
    
    class SustainedRateThrottle(UserRateThrottle):
        scope = 'sustained'
        rate = '1000/day'
    Python

    Apply to ViewSet:

    from .throttles import BurstRateThrottle, SustainedRateThrottle
    
    class BookViewSet(viewsets.ModelViewSet):
        throttle_classes = [BurstRateThrottle, SustainedRateThrottle]
    Python

    2. API Versioning

    # bookapi/settings.py
    
    REST_FRAMEWORK = {
        # ... existing settings
        'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
        'DEFAULT_VERSION': 'v1',
        'ALLOWED_VERSIONS': ['v1', 'v2'],
    }
    Python

    URLs:

    # bookapi/urls.py
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('api/v1/', include('books.urls')),
        path('api/v2/', include('books.urls_v2')),  # Different version
    ]
    Python

    Version-specific logic:

    class BookViewSet(viewsets.ModelViewSet):
        def get_serializer_class(self):
            if self.request.version == 'v2':
                return BookDetailSerializerV2
            return BookDetailSerializer
    Python

    3. Caching

    # Install django-redis
    pip install django-redis
    
    # bookapi/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.utils.decorators import method_decorator
    from django.views.decorators.cache import cache_page
    
    class BookViewSet(viewsets.ModelViewSet):
        @method_decorator(cache_page(60 * 15))  # Cache for 15 minutes
        def list(self, request, *args, **kwargs):
            return super().list(request, *args, **kwargs)
    Python

    4. File Upload

    # Add to Book model
    class Book(models.Model):
        # ... existing fields
        cover_image = models.ImageField(upload_to='book_covers/', blank=True, null=True)
        pdf_file = models.FileField(upload_to='book_pdfs/', blank=True, null=True)
    
    # Install Pillow
    pip install Pillow
    
    # Update settings.py
    MEDIA_URL = '/media/'
    MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
    
    # Update urls.py
    from django.conf import settings
    from django.conf.urls.static import static
    
    urlpatterns = [
        # ... existing urls
    ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    
    # Serializer
    class BookDetailSerializer(serializers.ModelSerializer):
        cover_image = serializers.ImageField(required=False)
        cover_image_url = serializers.SerializerMethodField()
    
        def get_cover_image_url(self, obj):
            if obj.cover_image:
                request = self.context.get('request')
                return request.build_absolute_uri(obj.cover_image.url)
            return None
    Python

    5. Nested Relationships

    # Create a nested serializer
    class BookWithReviewsSerializer(serializers.ModelSerializer):
        reviews = ReviewSerializer(many=True, read_only=True)
        author = AuthorSerializer(read_only=True)
    
        class Meta:
            model = Book
            fields = '__all__'
    Python

    6. Custom Actions with Different Methods

    class BookViewSet(viewsets.ModelViewSet):
        @action(detail=True, methods=['post', 'delete'])
        def favorite(self, request, pk=None):
            book = self.get_object()
            user = request.user
    
            if request.method == 'POST':
                # Add to favorites
                user.profile.favorite_books.add(book)
                return Response({'status': 'book added to favorites'})
    
            elif request.method == 'DELETE':
                # Remove from favorites
                user.profile.favorite_books.remove(book)
                return Response({'status': 'book removed from favorites'})
    Python

    7. Bulk Operations

    from rest_framework.decorators import api_view
    from rest_framework.response import Response
    
    @api_view(['POST'])
    def bulk_create_books(request):
        serializer = BookDetailSerializer(data=request.data, many=True)
        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)
    
    @api_view(['PATCH'])
    def bulk_update_books(request):
        book_ids = request.data.get('ids', [])
        update_data = request.data.get('data', {})
    
        books = Book.objects.filter(id__in=book_ids)
        books.update(**update_data)
    
        return Response({'updated': books.count()})
    Python

    8. API Documentation with drf-spectacular

    pip install drf-spectacular
    Bash
    # settings.py
    INSTALLED_APPS = [
        # ...
        'drf_spectacular',
    ]
    
    REST_FRAMEWORK = {
        # ...
        'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
    }
    
    SPECTACULAR_SETTINGS = {
        'TITLE': 'Book Management API',
        'DESCRIPTION': 'A comprehensive API for managing books and authors',
        'VERSION': '1.0.0',
        'SERVE_INCLUDE_SCHEMA': False,
    }
    
    # urls.py
    from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
    
    urlpatterns = [
        # ...
        path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
        path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
    ]
    Python

    Deployment

    1. Prepare for Production

    Update settings.py:

    # bookapi/settings.py
    
    import os
    from pathlib import Path
    
    # SECURITY WARNING: keep the secret key used in production secret!
    SECRET_KEY = os.environ.get('SECRET_KEY', 'your-secret-key-here')
    
    # SECURITY WARNING: don't run with debug turned on in production!
    DEBUG = os.environ.get('DEBUG', 'False') == 'True'
    
    ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
    
    # Database
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.postgresql',
            'NAME': os.environ.get('DB_NAME', 'bookapi'),
            'USER': os.environ.get('DB_USER', 'postgres'),
            'PASSWORD': os.environ.get('DB_PASSWORD', 'password'),
            'HOST': os.environ.get('DB_HOST', 'localhost'),
            'PORT': os.environ.get('DB_PORT', '5432'),
        }
    }
    
    # Static files
    STATIC_URL = '/static/'
    STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
    
    # Security settings
    SECURE_SSL_REDIRECT = not DEBUG
    SESSION_COOKIE_SECURE = not DEBUG
    CSRF_COOKIE_SECURE = not DEBUG
    SECURE_BROWSER_XSS_FILTER = True
    SECURE_CONTENT_TYPE_NOSNIFF = True
    X_FRAME_OPTIONS = 'DENY'
    Python

    2. Environment Variables (.env)

    # .env file
    SECRET_KEY=your-very-secret-key-here
    DEBUG=False
    ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
    DB_NAME=bookapi_prod
    DB_USER=prod_user
    DB_PASSWORD=secure_password
    DB_HOST=db.yourdomain.com
    DB_PORT=5432
    Python

    Install python-decouple:

    pip install python-decouple
    Bash

    Use in settings:

    from decouple import config
    
    SECRET_KEY = config('SECRET_KEY')
    DEBUG = config('DEBUG', default=False, cast=bool)
    Python

    3. Docker Deployment

    Dockerfile:

    FROM python:3.11-slim
    
    ENV PYTHONUNBUFFERED=1
    ENV PYTHONDONTWRITEBYTECODE=1
    
    WORKDIR /app
    
    # Install system dependencies
    RUN apt-get update && apt-get install -y \
        postgresql-client \
        && rm -rf /var/lib/apt/lists/*
    
    # Install Python dependencies
    COPY requirements.txt /app/
    RUN pip install --upgrade pip && pip install -r requirements.txt
    
    # Copy project
    COPY . /app/
    
    # Collect static files
    RUN python manage.py collectstatic --noinput
    
    # Run migrations and start server
    CMD ["gunicorn", "bookapi.wsgi:application", "--bind", "0.0.0.0:8000"]
    Dockerfile

    docker-compose.yml:

    version: '3.8'
    
    services:
      db:
        image: postgres:15
        volumes:
          - postgres_data:/var/lib/postgresql/data
        environment:
          - POSTGRES_DB=bookapi
          - POSTGRES_USER=postgres
          - POSTGRES_PASSWORD=postgres
        ports:
          - "5432:5432"
    
      web:
        build: .
        command: gunicorn bookapi.wsgi:application --bind 0.0.0.0:8000
        volumes:
          - .:/app
          - static_volume:/app/staticfiles
          - media_volume:/app/media
        ports:
          - "8000:8000"
        env_file:
          - .env
        depends_on:
          - db
    
      nginx:
        image: nginx:alpine
        volumes:
          - ./nginx.conf:/etc/nginx/nginx.conf
          - static_volume:/app/staticfiles
          - media_volume:/app/media
        ports:
          - "80:80"
        depends_on:
          - web
    
    volumes:
      postgres_data:
      static_volume:
      media_volume:
    Dockerfile

    4. Gunicorn Configuration

    pip install gunicorn
    Bash

    gunicorn_config.py:

    bind = "0.0.0.0:8000"
    workers = 4
    worker_class = "sync"
    worker_connections = 1000
    keepalive = 5
    errorlog = "-"
    accesslog = "-"
    loglevel = "info"
    Bash

    5. Nginx Configuration

    # nginx.conf
    upstream bookapi {
        server web:8000;
    }
    
    server {
        listen 80;
        server_name yourdomain.com;
    
        location / {
            proxy_pass http://bookapi;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;
            proxy_redirect off;
        }
    
        location /static/ {
            alias /app/staticfiles/;
        }
    
        location /media/ {
            alias /app/media/;
        }
    }
    Nginx

    6. Deploy Commands

    # Build and run with Docker Compose
    docker-compose up -d --build
    
    # Run migrations
    docker-compose exec web python manage.py migrate
    
    # Create superuser
    docker-compose exec web python manage.py createsuperuser
    
    # Collect static files
    docker-compose exec web python manage.py collectstatic --noinput
    
    # View logs
    docker-compose logs -f web
    Bash

    Best Practices

    1. Code Organization

    books/
    ├── migrations/
    ├── management/
       └── commands/
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── filters.py
    ├── models.py
    ├── pagination.py
    ├── permissions.py
    ├── serializers.py
    ├── tests.py
    ├── throttles.py
    ├── urls.py
    └── views.py
    Bash

    2. Serializer Best Practices

    ✅ DO:

    # Use different serializers for different actions
    class BookViewSet(viewsets.ModelViewSet):
        def get_serializer_class(self):
            if self.action == 'list':
                return BookListSerializer
            return BookDetailSerializer
    
    # Add validation
    def validate_isbn(self, value):
        if not value.isdigit():
            raise serializers.ValidationError("ISBN must contain only digits")
        return value
    
    # Use SerializerMethodField for computed fields
    books_count = serializers.SerializerMethodField()
    Python

    ❌ DON’T:

    # Don't expose sensitive fields
    class UserSerializer(serializers.ModelSerializer):
        class Meta:
            model = User
            fields = '__all__'  # BAD: Exposes password hash
    
    # Don't perform database queries in serializers
    def get_books(self, obj):
        return [book.title for book in Book.objects.all()]  # BAD: N+1 query
    Python

    3. ViewSet Best Practices

    ✅ DO:

    # Use select_related and prefetch_related
    queryset = Book.objects.select_related('author').prefetch_related('reviews')
    
    # Override perform_create for custom logic
    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)
    
    # Use get_queryset for dynamic filtering
    def get_queryset(self):
        if self.request.user.is_staff:
            return Book.objects.all()
        return Book.objects.filter(in_stock=True)
    Python

    ❌ DON’T:

    # Don't query database in loops
    for book in books:
        print(book.author.name)  # BAD: N+1 query
    
    # Don't skip permissions
    permission_classes = []  # BAD: No security
    Python

    4. Security Best Practices

    # 1. Always validate input
    def validate(self, data):
        if data['price'] < 0:
            raise serializers.ValidationError("Price cannot be negative")
        return data
    
    # 2. Use permissions properly
    from rest_framework.permissions import IsAuthenticated
    
    class BookViewSet(viewsets.ModelViewSet):
        permission_classes = [IsAuthenticated]
    
    # 3. Sanitize user input
    from django.utils.html import escape
    
    description = escape(request.data.get('description'))
    
    # 4. Use HTTPS in production
    SECURE_SSL_REDIRECT = True
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SECURE = True
    
    # 5. Rate limiting
    REST_FRAMEWORK = {
        'DEFAULT_THROTTLE_CLASSES': [
            'rest_framework.throttling.AnonRateThrottle',
            'rest_framework.throttling.UserRateThrottle'
        ]
    }
    Python

    5. Performance Best Practices

    # 1. Use database indexes
    class Book(models.Model):
        class Meta:
            indexes = [
                models.Index(fields=['title']),
                models.Index(fields=['isbn']),
            ]
    
    # 2. Optimize queries
    # BAD
    books = Book.objects.all()
    for book in books:
        print(book.author.name)  # N+1 query
    
    # GOOD
    books = Book.objects.select_related('author').all()
    for book in books:
        print(book.author.name)
    
    # 3. Use pagination
    pagination_class = PageNumberPagination
    
    # 4. Cache expensive queries
    from django.core.cache import cache
    
    def get_popular_books():
        books = cache.get('popular_books')
        if books is None:
            books = Book.objects.annotate(
                view_count=Count('views')
            ).order_by('-view_count')[:10]
            cache.set('popular_books', books, 60 * 60)  # Cache for 1 hour
        return books
    Python

    6. Testing Best Practices

    # 1. Test all CRUD operations
    def test_create_book(self):
        # Arrange
        self.client.force_authenticate(user=self.user)
        data = {...}
    
        # Act
        response = self.client.post('/api/books/', data)
    
        # Assert
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
    
    # 2. Test permissions
    def test_unauthenticated_cannot_create(self):
        response = self.client.post('/api/books/', {})
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
    
    # 3. Test validation
    def test_invalid_isbn(self):
        data = {'isbn': 'invalid'}
        serializer = BookSerializer(data=data)
        self.assertFalse(serializer.is_valid())
    
    # 4. Use fixtures
    def setUp(self):
        self.author = Author.objects.create(name="Test Author")
        self.book = Book.objects.create(title="Test Book", author=self.author)
    Python

    7. API Design Best Practices

    RESTful URLs:

    ✅ GOOD:
    GET    /api/books/              # List all books
    POST   /api/books/              # Create a book
    GET    /api/books/{id}/         # Get a specific book
    PUT    /api/books/{id}/         # Update a book
    PATCH  /api/books/{id}/         # Partial update
    DELETE /api/books/{id}/         # Delete a book
    GET    /api/authors/{id}/books/ # Get books by author
    
    ❌ BAD:
    GET /api/getAllBooks/
    POST /api/createBook/
    GET /api/book?id=1
    Python

    Response Format:

    # Consistent response structure
    {
        "status": "success",
        "data": {...},
        "message": "Book created successfully"
    }
    
    # Error responses
    {
        "status": "error",
        "errors": {
            "field": ["Error message"]
        },
        "message": "Validation failed"
    }
    Python

    8. Documentation

    # Document your viewsets
    class BookViewSet(viewsets.ModelViewSet):
        """
        API endpoint for managing books.
    
        list: Return a list of all books
        create: Create a new book
        retrieve: Return a specific book
        update: Update a book
        partial_update: Partially update a book
        destroy: Delete a book
        """
    
        @action(detail=True, methods=['post'])
        def toggle_stock(self, request, pk=None):
            """
            Toggle the in_stock status of a book.
    
            Returns the updated book data.
            """
            pass
    Python

    Conclusion

    You now have a comprehensive understanding of building Django REST APIs! This tutorial covered:

    Setup and Configuration – Project structure and dependencies ✅ Models and Database – Creating models with relationships ✅ Serializers – Data validation and transformation ✅ ViewSets and URLs – API endpoints and routing ✅ Authentication – Token and JWT authentication ✅ Permissions – Access control and security ✅ Filtering and Search – Query optimization ✅ Pagination – Managing large datasets ✅ Testing – Unit and integration tests ✅ Advanced Features – Caching, throttling, versioning ✅ Deployment – Production-ready setup ✅ Best Practices – Code quality and performance

    Next Steps

    1. Practice: Build your own API project
    2. Explore: Django Channels for WebSockets
    3. Learn: GraphQL with Graphene-Django
    4. Study: Microservices architecture
    5. Contribute: Open-source Django projects

    Useful Resources

    Common Commands Reference

    # Start project
    django-admin startproject projectname
    python manage.py startapp appname
    
    # Database
    python manage.py makemigrations
    python manage.py migrate
    python manage.py createsuperuser
    
    # Run server
    python manage.py runserver
    python manage.py runserver 0.0.0.0:8000
    
    # Testing
    python manage.py test
    python manage.py test appname
    
    # Shell
    python manage.py shell
    python manage.py dbshell
    
    # Static files
    python manage.py collectstatic
    
    # Show URLs
    python manage.py show_urls  # requires django-extensions
    Bash

    Django REST Framework Cheatsheet

    Quick Start Commands

    # Installation
    pip install django djangorestframework
    pip install djangorestframework-simplejwt django-filter django-cors-headers
    
    # Project Setup
    django-admin startproject myproject
    cd myproject
    python manage.py startapp myapp
    
    # Database
    python manage.py makemigrations
    python manage.py migrate
    python manage.py createsuperuser
    
    # Run Server
    python manage.py runserver
    python manage.py runserver 8080
    
    # Shell
    python manage.py shell
    python manage.py shell_plus  # requires django-extensions
    
    # Testing
    python manage.py test
    python manage.py test myapp.tests.TestClass
    coverage run --source='.' manage.py test
    coverage report
    
    # Production
    python manage.py collectstatic
    python manage.py check --deploy
    gunicorn myproject.wsgi:application
    Bash

    Settings Configuration

    # settings.py - Essential DRF Configuration
    
    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'rest_framework',
        'rest_framework.authtoken',
        'django_filters',
        'corsheaders',
        'myapp',
    ]
    
    MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'corsheaders.middleware.CorsMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ]
    
    # REST Framework Settings
    REST_FRAMEWORK = {
        # Authentication
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework.authentication.SessionAuthentication',
            'rest_framework.authentication.TokenAuthentication',
            'rest_framework_simplejwt.authentication.JWTAuthentication',
        ],
    
        # Permissions
        'DEFAULT_PERMISSION_CLASSES': [
            'rest_framework.permissions.IsAuthenticatedOrReadOnly',
        ],
    
        # Pagination
        'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
        'PAGE_SIZE': 10,
    
        # Filtering
        'DEFAULT_FILTER_BACKENDS': [
            'django_filters.rest_framework.DjangoFilterBackend',
            'rest_framework.filters.SearchFilter',
            'rest_framework.filters.OrderingFilter',
        ],
    
        # Throttling
        'DEFAULT_THROTTLE_CLASSES': [
            'rest_framework.throttling.AnonRateThrottle',
            'rest_framework.throttling.UserRateThrottle',
        ],
        'DEFAULT_THROTTLE_RATES': {
            'anon': '100/day',
            'user': '1000/day',
        },
    
        # Versioning
        'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
    
        # Rendering
        'DEFAULT_RENDERER_CLASSES': [
            'rest_framework.renderers.JSONRenderer',
            'rest_framework.renderers.BrowsableAPIRenderer',
        ],
    
        # Parser
        'DEFAULT_PARSER_CLASSES': [
            'rest_framework.parsers.JSONParser',
            'rest_framework.parsers.FormParser',
            'rest_framework.parsers.MultiPartParser',
        ],
    }
    
    # CORS Settings
    CORS_ALLOWED_ORIGINS = [
        "http://localhost:3000",
        "http://127.0.0.1:3000",
    ]
    
    # JWT Settings
    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': True,
    }
    Python

    Models Quick Reference

    # models.py - Common Model Patterns
    
    from django.db import models
    from django.contrib.auth.models import User
    from django.core.validators import MinValueValidator, MaxValueValidator
    
    # Basic Model
    class Item(models.Model):
        name = models.CharField(max_length=200)
        description = models.TextField(blank=True)
        created_at = models.DateTimeField(auto_now_add=True)
        updated_at = models.DateTimeField(auto_now=True)
    
        class Meta:
            ordering = ['-created_at']
            verbose_name_plural = 'Items'
    
        def __str__(self):
            return self.name
    
    # Field Types
    class Product(models.Model):
        # Text fields
        title = models.CharField(max_length=200)
        slug = models.SlugField(unique=True)
        description = models.TextField()
        short_desc = models.CharField(max_length=500, blank=True)
    
        # Numeric fields
        price = models.DecimalField(max_digits=10, decimal_places=2)
        quantity = models.IntegerField(default=0)
        rating = models.FloatField(validators=[MinValueValidator(0), MaxValueValidator(5)])
    
        # Boolean
        is_active = models.BooleanField(default=True)
        is_featured = models.BooleanField(default=False)
    
        # Date/Time
        created_at = models.DateTimeField(auto_now_add=True)
        updated_at = models.DateTimeField(auto_now=True)
        published_date = models.DateField(null=True, blank=True)
    
        # File fields
        image = models.ImageField(upload_to='products/', blank=True, null=True)
        document = models.FileField(upload_to='docs/', blank=True, null=True)
    
        # Choices
        STATUS_CHOICES = [
            ('draft', 'Draft'),
            ('published', 'Published'),
            ('archived', 'Archived'),
        ]
        status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
    
        # JSON field
        metadata = models.JSONField(default=dict, blank=True)
    
    # Relationships
    class Author(models.Model):
        name = models.CharField(max_length=200)
    
    class Book(models.Model):
        # Foreign Key (Many-to-One)
        author = models.ForeignKey(
            Author, 
            on_delete=models.CASCADE,
            related_name='books'
        )
    
        # One-to-One
        isbn_info = models.OneToOneField(
            'ISBNInfo',
            on_delete=models.CASCADE,
            null=True
        )
    
        # Many-to-Many
        categories = models.ManyToManyField('Category', related_name='books')
    
    # Custom Manager
    class PublishedManager(models.Manager):
        def get_queryset(self):
            return super().get_queryset().filter(status='published')
    
    class Post(models.Model):
        title = models.CharField(max_length=200)
        status = models.CharField(max_length=20)
    
        objects = models.Manager()  # Default manager
        published = PublishedManager()  # Custom manager
    
    # Abstract Base Class
    class TimeStampedModel(models.Model):
        created_at = models.DateTimeField(auto_now_add=True)
        updated_at = models.DateTimeField(auto_now=True)
    
        class Meta:
            abstract = True
    
    class MyModel(TimeStampedModel):
        name = models.CharField(max_length=200)
    
    # Indexes and Constraints
    class Article(models.Model):
        title = models.CharField(max_length=200)
        slug = models.SlugField()
    
        class Meta:
            indexes = [
                models.Index(fields=['title']),
                models.Index(fields=['slug', 'title']),
            ]
            constraints = [
                models.UniqueConstraint(fields=['title', 'slug'], name='unique_title_slug')
            ]
    Python

    Serializers Cheatsheet

    # serializers.py - All Serializer Patterns
    
    from rest_framework import serializers
    from .models import Product, Author, Book
    
    # Basic ModelSerializer
    class ProductSerializer(serializers.ModelSerializer):
        class Meta:
            model = Product
            fields = '__all__'  # All fields
            # fields = ['id', 'name', 'price']  # Specific fields
            # exclude = ['created_at']  # Exclude fields
            read_only_fields = ['id', 'created_at']
    
    # Serializer with Custom Fields
    class BookSerializer(serializers.ModelSerializer):
        # Read-only computed field
        author_name = serializers.CharField(source='author.name', read_only=True)
    
        # SerializerMethodField
        is_new = serializers.SerializerMethodField()
        page_count = serializers.SerializerMethodField()
    
        # Write-only field
        author_id = serializers.IntegerField(write_only=True)
    
        class Meta:
            model = Book
            fields = ['id', 'title', 'author_name', 'author_id', 'is_new', 'page_count']
    
        def get_is_new(self, obj):
            from datetime import datetime, timedelta
            return obj.published_date > datetime.now().date() - timedelta(days=30)
    
        def get_page_count(self, obj):
            return obj.pages
    
    # Nested Serializers
    class AuthorDetailSerializer(serializers.ModelSerializer):
        books = BookSerializer(many=True, read_only=True)
        book_count = serializers.IntegerField(source='books.count', read_only=True)
    
        class Meta:
            model = Author
            fields = ['id', 'name', 'books', 'book_count']
    
    # Validation
    class ProductValidationSerializer(serializers.ModelSerializer):
        class Meta:
            model = Product
            fields = '__all__'
    
        # Field-level validation
        def validate_price(self, value):
            if value < 0:
                raise serializers.ValidationError("Price cannot be negative")
            return value
    
        def validate_title(self, value):
            if len(value) < 3:
                raise serializers.ValidationError("Title must be at least 3 characters")
            return value
    
        # Object-level validation
        def validate(self, data):
            if data.get('price', 0) > 1000 and not data.get('is_premium', False):
                raise serializers.ValidationError(
                    "Products over $1000 must be marked as premium"
                )
            return data
    
        # Create override
        def create(self, validated_data):
            # Custom creation logic
            instance = Product.objects.create(**validated_data)
            return instance
    
        # Update override
        def update(self, instance, validated_data):
            # Custom update logic
            instance.title = validated_data.get('title', instance.title)
            instance.save()
            return instance
    
    # Different Serializers for Different Actions
    class ProductListSerializer(serializers.ModelSerializer):
        """Lightweight for list views"""
        class Meta:
            model = Product
            fields = ['id', 'title', 'price']
    
    class ProductDetailSerializer(serializers.ModelSerializer):
        """Detailed for single item"""
        class Meta:
            model = Product
            fields = '__all__'
    
    # HyperlinkedModelSerializer
    class BookHyperlinkedSerializer(serializers.HyperlinkedModelSerializer):
        class Meta:
            model = Book
            fields = ['url', 'title', 'author']
    
    # Custom Serializer (non-model)
    class CustomSerializer(serializers.Serializer):
        email = serializers.EmailField()
        content = serializers.CharField(max_length=200)
        created = serializers.DateTimeField()
    
        def create(self, validated_data):
            return CustomObject(**validated_data)
    
        def update(self, instance, validated_data):
            instance.email = validated_data.get('email', instance.email)
            return instance
    
    # StringRelatedField, PrimaryKeyRelatedField, SlugRelatedField
    class BookRelationSerializer(serializers.ModelSerializer):
        # Shows __str__ of related object
        author = serializers.StringRelatedField()
    
        # Shows primary key
        # author = serializers.PrimaryKeyRelatedField(queryset=Author.objects.all())
    
        # Shows specific field
        # author = serializers.SlugRelatedField(
        #     slug_field='name',
        #     queryset=Author.objects.all()
        # )
    
        class Meta:
            model = Book
            fields = ['id', 'title', 'author']
    
    # Writable Nested Serializers
    class AuthorWritableSerializer(serializers.ModelSerializer):
        books = BookSerializer(many=True)
    
        class Meta:
            model = Author
            fields = ['id', 'name', 'books']
    
        def create(self, validated_data):
            books_data = validated_data.pop('books')
            author = Author.objects.create(**validated_data)
            for book_data in books_data:
                Book.objects.create(author=author, **book_data)
            return author
    Python

    Views and ViewSets Cheatsheet

    # views.py - All View Patterns
    
    from rest_framework import viewsets, views, generics, status
    from rest_framework.decorators import action, api_view, permission_classes
    from rest_framework.response import Response
    from rest_framework.permissions import IsAuthenticated, AllowAny
    from django_filters.rest_framework import DjangoFilterBackend
    from rest_framework import filters
    
    # 1. APIView (Most Control)
    class ProductAPIView(views.APIView):
        permission_classes = [IsAuthenticated]
    
        def get(self, request):
            products = Product.objects.all()
            serializer = ProductSerializer(products, many=True)
            return Response(serializer.data)
    
        def post(self, request):
            serializer = ProductSerializer(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)
    
    # 2. Generic Views
    class ProductListView(generics.ListAPIView):
        queryset = Product.objects.all()
        serializer_class = ProductSerializer
    
    class ProductCreateView(generics.CreateAPIView):
        queryset = Product.objects.all()
        serializer_class = ProductSerializer
    
    class ProductDetailView(generics.RetrieveAPIView):
        queryset = Product.objects.all()
        serializer_class = ProductSerializer
    
    class ProductUpdateView(generics.UpdateAPIView):
        queryset = Product.objects.all()
        serializer_class = ProductSerializer
    
    class ProductDeleteView(generics.DestroyAPIView):
        queryset = Product.objects.all()
        serializer_class = ProductSerializer
    
    # Combined Generic Views
    class ProductListCreateView(generics.ListCreateAPIView):
        queryset = Product.objects.all()
        serializer_class = ProductSerializer
    
    class ProductRetrieveUpdateView(generics.RetrieveUpdateAPIView):
        queryset = Product.objects.all()
        serializer_class = ProductSerializer
    
    class ProductRetrieveDestroyView(generics.RetrieveDestroyAPIView):
        queryset = Product.objects.all()
        serializer_class = ProductSerializer
    
    class ProductRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView):
        queryset = Product.objects.all()
        serializer_class = ProductSerializer
    
    # 3. ViewSets (Most Common)
    class ProductViewSet(viewsets.ModelViewSet):
        """
        ModelViewSet provides:
        - list() - GET /products/
        - create() - POST /products/
        - retrieve() - GET /products/{id}/
        - update() - PUT /products/{id}/
        - partial_update() - PATCH /products/{id}/
        - destroy() - DELETE /products/{id}/
        """
        queryset = Product.objects.all()
        serializer_class = ProductSerializer
        permission_classes = [IsAuthenticated]
        filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
        filterset_fields = ['category', 'is_active']
        search_fields = ['title', 'description']
        ordering_fields = ['price', 'created_at']
    
        # Custom queryset
        def get_queryset(self):
            queryset = Product.objects.all()
            if self.request.user.is_staff:
                return queryset
            return queryset.filter(is_active=True)
    
        # Different serializers for different actions
        def get_serializer_class(self):
            if self.action == 'list':
                return ProductListSerializer
            return ProductDetailSerializer
    
        # Override create
        def perform_create(self, serializer):
            serializer.save(created_by=self.request.user)
    
        # Override update
        def perform_update(self, serializer):
            serializer.save(updated_by=self.request.user)
    
        # Custom action - detail=True (requires pk)
        @action(detail=True, methods=['post'])
        def activate(self, request, pk=None):
            product = self.get_object()
            product.is_active = True
            product.save()
            return Response({'status': 'product activated'})
    
        # Custom action - detail=False (no pk required)
        @action(detail=False, methods=['get'])
        def featured(self, request):
            featured = self.queryset.filter(is_featured=True)
            serializer = self.get_serializer(featured, many=True)
            return Response(serializer.data)
    
        # Custom action with permission
        @action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
        def purchase(self, request, pk=None):
            product = self.get_object()
            # Purchase logic
            return Response({'status': 'purchased'})
    
    # ReadOnlyModelViewSet
    class ProductReadOnlyViewSet(viewsets.ReadOnlyModelViewSet):
        """Provides only list() and retrieve()"""
        queryset = Product.objects.all()
        serializer_class = ProductSerializer
    
    # Custom ViewSet
    class CustomProductViewSet(viewsets.ViewSet):
        """Manual implementation of all actions"""
    
        def list(self, request):
            products = Product.objects.all()
            serializer = ProductSerializer(products, many=True)
            return Response(serializer.data)
    
        def create(self, request):
            serializer = ProductSerializer(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)
    
        def retrieve(self, request, pk=None):
            product = get_object_or_404(Product, pk=pk)
            serializer = ProductSerializer(product)
            return Response(serializer.data)
    
        def update(self, request, pk=None):
            product = get_object_or_404(Product, pk=pk)
            serializer = ProductSerializer(product, data=request.data)
            if serializer.is_valid():
                serializer.save()
                return Response(serializer.data)
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
        def destroy(self, request, pk=None):
            product = get_object_or_404(Product, pk=pk)
            product.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)
    
    # 4. Function-Based Views
    @api_view(['GET', 'POST'])
    @permission_classes([IsAuthenticated])
    def product_list(request):
        if request.method == 'GET':
            products = Product.objects.all()
            serializer = ProductSerializer(products, many=True)
            return Response(serializer.data)
    
        elif request.method == 'POST':
            serializer = ProductSerializer(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)
    
    @api_view(['GET', 'PUT', 'DELETE'])
    def product_detail(request, pk):
        try:
            product = Product.objects.get(pk=pk)
        except Product.DoesNotExist:
            return Response(status=status.HTTP_404_NOT_FOUND)
    
        if request.method == 'GET':
            serializer = ProductSerializer(product)
            return Response(serializer.data)
    
        elif request.method == 'PUT':
            serializer = ProductSerializer(product, data=request.data)
            if serializer.is_valid():
                serializer.save()
                return Response(serializer.data)
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
        elif request.method == 'DELETE':
            product.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)
    
    # Response Status Codes
    from rest_framework import status
    
    Response(data, status=status.HTTP_200_OK)
    Response(data, status=status.HTTP_201_CREATED)
    Response(status=status.HTTP_204_NO_CONTENT)
    Response(errors, status=status.HTTP_400_BAD_REQUEST)
    Response(status=status.HTTP_401_UNAUTHORIZED)
    Response(status=status.HTTP_403_FORBIDDEN)
    Response(status=status.HTTP_404_NOT_FOUND)
    Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
    Python

    URL Routing Cheatsheet

    # urls.py - All Routing Patterns
    
    from django.urls import path, include
    from rest_framework.routers import DefaultRouter, SimpleRouter
    from . import views
    
    # 1. Using Routers (for ViewSets)
    router = DefaultRouter()  # Includes API root view
    # router = SimpleRouter()  # No API root view
    
    router.register(r'products', views.ProductViewSet, basename='product')
    router.register(r'authors', views.AuthorViewSet)
    
    urlpatterns = [
        path('', include(router.urls)),
    ]
    
    # Generated URLs:
    # GET    /products/          -> list
    # POST   /products/          -> create
    # GET    /products/{id}/     -> retrieve
    # PUT    /products/{id}/     -> update
    # PATCH  /products/{id}/     -> partial_update
    # DELETE /products/{id}/     -> destroy
    # GET    /products/featured/ -> custom action
    
    # 2. Manual URL Patterns
    urlpatterns = [
        # Function-based views
        path('products/', views.product_list),
        path('products/<int:pk>/', views.product_detail),
    
        # Class-based views
        path('products/', views.ProductListView.as_view()),
        path('products/<int:pk>/', views.ProductDetailView.as_view()),
    
        # APIView
        path('products/', views.ProductAPIView.as_view()),
    
        # Generic views
        path('products/', views.ProductListCreateView.as_view(), name='product-list'),
        path('products/<int:pk>/', views.ProductRetrieveUpdateDestroyView.as_view(), name='product-detail'),
    ]
    
    # 3. ViewSet with manual routing
    from rest_framework import routers
    
    product_list = views.ProductViewSet.as_view({
        'get': 'list',
        'post': 'create'
    })
    
    product_detail = views.ProductViewSet.as_view({
        'get': 'retrieve',
        'put': 'update',
        'patch': 'partial_update',
        'delete': 'destroy'
    })
    
    urlpatterns = [
        path('products/', product_list, name='product-list'),
        path('products/<int:pk>/', product_detail, name='product-detail'),
    ]
    
    # 4. Nested Routes
    urlpatterns = [
        path('authors/<int:author_pk>/books/', views.BookListView.as_view()),
        path('authors/<int:author_pk>/books/<int:pk>/', views.BookDetailView.as_view()),
    ]
    
    # 5. App-level URLs
    # myapp/urls.py
    app_name = 'myapp'
    
    urlpatterns = [
        path('products/', views.ProductListView.as_view(), name='product-list'),
    ]
    
    # Project urls.py
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('api/', include('myapp.urls')),
        path('api-auth/', include('rest_framework.urls')),
    ]
    
    # 6. Versioning URLs
    urlpatterns = [
        path('api/v1/', include('myapp.urls_v1')),
        path('api/v2/', include('myapp.urls_v2')),
    ]
    
    # 7. Custom Router
    router = DefaultRouter()
    router.register(r'products', views.ProductViewSet)
    
    # Add extra routes
    urlpatterns = [
        path('', include(router.urls)),
        path('custom/', views.CustomView.as_view()),
    ]
    Python

    Authentication Cheatsheet

    # ============================================
    # 1. SESSION AUTHENTICATION (Default)
    # ============================================
    # settings.py
    REST_FRAMEWORK = {
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework.authentication.SessionAuthentication',
        ]
    }
    
    # Login via browsable API or:
    # POST /api-auth/login/
    
    # ============================================
    # 2. TOKEN AUTHENTICATION
    # ============================================
    # settings.py
    INSTALLED_APPS = [
        'rest_framework.authtoken',
    ]
    
    REST_FRAMEWORK = {
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework.authentication.TokenAuthentication',
        ]
    }
    
    # Run migrations
    python manage.py migrate
    
    # Create tokens for existing users
    from django.contrib.auth.models import User
    from rest_framework.authtoken.models import Token
    
    for user in User.objects.all():
        Token.objects.get_or_create(user=user)
    
    # urls.py
    from rest_framework.authtoken.views import obtain_auth_token
    
    urlpatterns = [
        path('api/token/', obtain_auth_token),
    ]
    
    # Get token
    # POST /api/token/
    # Body: {"username": "user", "password": "pass"}
    # Response: {"token": "abc123..."}
    
    # Use token in requests
    # Header: Authorization: Token abc123...
    
    # Custom obtain token view
    from rest_framework.authtoken.views import ObtainAuthToken
    from rest_framework.authtoken.models import Token
    from rest_framework.response import Response
    
    class CustomAuthToken(ObtainAuthToken):
        def post(self, request, *args, **kwargs):
            serializer = self.serializer_class(data=request.data)
            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,
                'email': user.email
            })
    
    # ============================================
    # 3. JWT AUTHENTICATION (Recommended)
    # ============================================
    # Install
    pip install djangorestframework-simplejwt
    
    # settings.py
    from datetime import timedelta
    
    INSTALLED_APPS = [
        'rest_framework_simplejwt',
    ]
    
    REST_FRAMEWORK = {
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework_simplejwt.authentication.JWTAuthentication',
        ],
    }
    
    SIMPLE_JWT = {
        'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
        'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
        'ROTATE_REFRESH_TOKENS': True,
        'BLACKLIST_AFTER_ROTATION': True,
        'UPDATE_LAST_LOGIN': False,
        'ALGORITHM': 'HS256',
        'SIGNING_KEY': SECRET_KEY,
        'AUTH_HEADER_TYPES': ('Bearer',),
    }
    
    # urls.py
    from rest_framework_simplejwt.views import (
        TokenObtainPairView,
        TokenRefreshView,
        TokenVerifyView,
    )
    
    urlpatterns = [
        path('api/token/', TokenObtainPairView.as_view()),
        path('api/token/refresh/', TokenRefreshView.as_view()),
        path('api/token/verify/', TokenVerifyView.as_view()),
    ]
    
    # Get tokens
    # POST /api/token/
    # Body: {"username": "user", "password": "pass"}
    # Response: {"access": "...", "refresh": "..."}
    
    # Refresh access token
    # POST /api/token/refresh/
    # Body: {"refresh": "..."}
    # Response: {"access": "..."}
    
    # Use in requests
    # Header: Authorization: Bearer <access_token>
    
    # Custom JWT Claims
    from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
    from rest_framework_simplejwt.views import TokenObtainPairView
    
    class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
        @classmethod
        def get_token(cls, user):
            token = super().get_token(user)
            # Add custom claims
            token['email'] = user.email
            token['is_staff'] = user.is_staff
            return token
    
    class CustomTokenObtainPairView(TokenObtainPairView):
        serializer_class = CustomTokenObtainPairSerializer
    
    # ============================================
    # 4. BASIC AUTHENTICATION
    # ============================================
    REST_FRAMEWORK = {
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework.authentication.BasicAuthentication',
        ]
    }
    
    # Use in requests
    # Header: Authorization: Basic <base64(username:password)>
    
    # ============================================
    # 5. CUSTOM AUTHENTICATION
    # ============================================
    from rest_framework.authentication import BaseAuthentication
    from rest_framework import exceptions
    
    class CustomAuthentication(BaseAuthentication):
        def authenticate(self, request):
            custom_token = request.META.get('HTTP_X_CUSTOM_TOKEN')
            if not custom_token:
                return None
    
            try:
                user = User.objects.get(custom_token=custom_token)
            except User.DoesNotExist:
                raise exceptions.AuthenticationFailed('Invalid token')
    
            return (user, None)
    
    # Use in views
    from rest_framework.authentication import CustomAuthentication
    
    class MyView(APIView):
        authentication_classes = [CustomAuthentication]
    
    # ============================================
    # USER REGISTRATION
    # ============================================
    from rest_framework import generics, status
    from rest_framework.response import Response
    from django.contrib.auth.models import User
    
    class RegisterView(generics.CreateAPIView):
        queryset = User.objects.all()
        permission_classes = []
    
        def post(self, request):
            username = request.data.get('username')
            password = request.data.get('password')
            email = request.data.get('email')
    
            if User.objects.filter(username=username).exists():
                return Response(
                    {'error': 'Username already exists'},
                    status=status.HTTP_400_BAD_REQUEST
                )
    
            user = User.objects.create_user(
                username=username,
                password=password,
                email=email
            )
    
            # Create token (if using token auth)
            from rest_framework.authtoken.models import Token
            token = Token.objects.create(user=user)
    
            return Response({
                'user_id': user.id,
                'username': user.username,
                'token': token.key
            }, status=status.HTTP_201_CREATED)
    Python

    Permissions Cheatsheet

    # ============================================
    # BUILT-IN PERMISSION CLASSES
    # ============================================
    from rest_framework.permissions import (
        AllowAny,
        IsAuthenticated,
        IsAuthenticatedOrReadOnly,
        IsAdminUser,
        DjangoModelPermissions,
        DjangoModelPermissionsOrAnonReadOnly,
    )
    
    # Apply globally in settings.py
    REST_FRAMEWORK = {
        'DEFAULT_PERMISSION_CLASSES': [
            'rest_framework.permissions.IsAuthenticatedOrReadOnly',
        ]
    }
    
    # Apply to specific view
    class ProductViewSet(viewsets.ModelViewSet):
        permission_classes = [IsAuthenticated]
    
    # Apply to specific action
    @action(detail=True, permission_classes=[IsAdminUser])
    def admin_only_action(self, request, pk=None):
        pass
    
    # ============================================
    # CUSTOM PERMISSIONS
    # ============================================
    from rest_framework import permissions
    
    # 1. Object-level permission
    class IsOwnerOrReadOnly(permissions.BasePermission):
        """
        Custom permission to only allow owners to edit objects.
        """
        message = 'You must be the owner to edit this.'
    
        def has_object_permission(self, request, view, obj):
            # Read permissions for any request
            if request.method in permissions.SAFE_METHODS:
                return True
    
            # Write permissions only to owner
            return obj.owner == request.user
    
    # 2. View-level permission
    class IsAdminOrReadOnly(permissions.BasePermission):
        """
        Custom permission to only allow admins to edit.
        """
    
        def has_permission(self, request, view):
            if request.method in permissions.SAFE_METHODS:
                return True
            return request.user and request.user.is_staff
    
    # 3. Combined permissions
    class IsOwnerAndAuthenticated(permissions.BasePermission):
        def has_object_permission(self, request, view, obj):
            return request.user.is_authenticated and obj.owner == request.user
    
    # 4. Conditional permissions
    class CanEditProduct(permissions.BasePermission):
        def has_object_permission(self, request, view, obj):
            # Owner can always edit
            if obj.owner == request.user:
                return True
    
            # Staff can edit if product is not published
            if request.user.is_staff and not obj.is_published:
                return True
    
            return False
    
    # Use multiple permissions (ALL must pass)
    class ProductViewSet(viewsets.ModelViewSet):
        permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
    
    # Custom permission logic
    from rest_framework.permissions import BasePermission
    
    class HasAPIKey(BasePermission):
        def has_permission(self, request, view):
            api_key = request.META.get('HTTP_X_API_KEY')
            return api_key == 'your-secret-key'
    
    # Per-action permissions
    from rest_framework.decorators import action
    
    class ProductViewSet(viewsets.ModelViewSet):
        def get_permissions(self):
            if self.action in ['create', 'update', 'partial_update', 'destroy']:
                permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
            else:
                permission_classes = [AllowAny]
            return [permission() for permission in permission_classes]
    
    # ============================================
    # PERMISSION EXAMPLES
    # ============================================
    
    # AllowAny - No restrictions
    permission_classes = [AllowAny]
    
    # IsAuthenticated - Must be logged in
    permission_classes = [IsAuthenticated]
    
    # IsAuthenticatedOrReadOnly - Read for all, write for authenticated
    permission_classes = [IsAuthenticatedOrReadOnly]
    
    # IsAdminUser - Only admin/staff users
    permission_classes = [IsAdminUser]
    
    # DjangoModelPermissions - Based on Django model permissions
    permission_classes = [DjangoModelPermissions]
    
    # Multiple permissions (AND logic)
    permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
    
    # Function-based view permissions
    @api_view(['GET'])
    @permission_classes([IsAuthenticated])
    def my_view(request):
        pass
    Python

    Filtering, Searching & Pagination Cheatsheet

    # ============================================
    # FILTERING
    # ============================================
    # Install
    pip install django-filter
    
    # settings.py
    INSTALLED_APPS = [
        'django_filters',
    ]
    
    REST_FRAMEWORK = {
        'DEFAULT_FILTER_BACKENDS': [
            'django_filters.rest_framework.DjangoFilterBackend',
        ]
    }
    
    # Simple filtering
    class ProductViewSet(viewsets.ModelViewSet):
        queryset = Product.objects.all()
        filterset_fields = ['category', 'price', 'is_active']
    
    # Usage: /api/products/?category=electronics&is_active=true
    
    # Advanced filtering
    from django_filters import rest_framework as filters
    
    class ProductFilter(filters.FilterSet):
        min_price = filters.NumberFilter(field_name='price', lookup_expr='gte')
        max_price = filters.NumberFilter(field_name='price', lookup_expr='lte')
        name = filters.CharFilter(lookup_expr='icontains')
        created_after = filters.DateFilter(field_name='created_at', lookup_expr='gte')
    
        class Meta:
            model = Product
            fields = ['category', 'is_active']
    
    class ProductViewSet(viewsets.ModelViewSet):
        queryset = Product.objects.all()
        filterset_class = ProductFilter
    
    # Usage: /api/products/?min_price=10&max_price=100&name=phone
    
    # Lookup expressions
    # exact, iexact, contains, icontains, startswith, istartswith
    # endswith, iendswith, gt, gte, lt, lte, in, range
    
    # ============================================
    # SEARCHING
    # ============================================
    from rest_framework import filters
    
    REST_FRAMEWORK = {
        'DEFAULT_FILTER_BACKENDS': [
            'rest_framework.filters.SearchFilter',
        ]
    }
    
    class ProductViewSet(viewsets.ModelViewSet):
        queryset = Product.objects.all()
        filter_backends = [filters.SearchFilter]
        search_fields = ['title', 'description', 'category__name']
        # search_fields = ['=username']  # Exact match
        # search_fields = ['^title']  # Starts with
        # search_fields = ['@description']  # Full-text search (PostgreSQL)
    
    # Usage: /api/products/?search=laptop
    
    # ============================================
    # ORDERING
    # ============================================
    from rest_framework import filters
    
    class ProductViewSet(viewsets.ModelViewSet):
        queryset = Product.objects.all()
        filter_backends = [filters.OrderingFilter]
        ordering_fields = ['price', 'created_at', 'title']
        ordering = ['-created_at']  # Default ordering
    
    # Usage: /api/products/?ordering=price  # Ascending
    # Usage: /api/products/?ordering=-price  # Descending
    # Usage: /api/products/?ordering=price,created_at  # Multiple
    
    # ============================================
    # PAGINATION
    # ============================================
    
    # 1. PageNumberPagination (default)
    from rest_framework.pagination import PageNumberPagination
    
    class StandardPagination(PageNumberPagination):
        page_size = 10
        page_size_query_param = 'page_size'
        max_page_size = 100
    
    class ProductViewSet(viewsets.ModelViewSet):
        pagination_class = StandardPagination
    
    # Usage: /api/products/?page=2&page_size=20
    
    # Response:
    {
        "count": 100,
        "next": "http://api.example.org/products/?page=3",
        "previous": "http://api.example.org/products/?page=1",
        "results": [...]
    }
    
    # 2. LimitOffsetPagination
    from rest_framework.pagination import LimitOffsetPagination
    
    class CustomLimitOffsetPagination(LimitOffsetPagination):
        default_limit = 10
        max_limit = 100
    
    # Usage: /api/products/?limit=10&offset=20
    
    # 3. CursorPagination (best for large datasets)
    from rest_framework.pagination import CursorPagination
    
    class CustomCursorPagination(CursorPagination):
        page_size = 10
        ordering = '-created_at'
    
    # Usage: /api/products/?cursor=cD0yMDIw
    
    # Global pagination
    REST_FRAMEWORK = {
        'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
        'PAGE_SIZE': 10
    }
    
    # Custom pagination response
    class CustomPagination(PageNumberPagination):
        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,
                'current_page': self.page.number,
                'results': data
            })
    
    # ============================================
    # COMBINING ALL
    # ============================================
    from django_filters.rest_framework import DjangoFilterBackend
    from rest_framework import filters
    
    class ProductViewSet(viewsets.ModelViewSet):
        queryset = Product.objects.all()
        serializer_class = ProductSerializer
        filter_backends = [
            DjangoFilterBackend,
            filters.SearchFilter,
            filters.OrderingFilter
        ]
        filterset_class = ProductFilter
        search_fields = ['title', 'description']
        ordering_fields = ['price', 'created_at']
        pagination_class = StandardPagination
    
    # Usage:
    # /api/products/?category=electronics&min_price=100&search=laptop&ordering=-price&page=2
    Python

    Testing Cheatsheet

    # tests.py - Complete Testing Guide
    
    from django.test import TestCase
    from django.contrib.auth.models import User
    from rest_framework.test import APITestCase, APIClient, APIRequestFactory
    from rest_framework import status
    from .models import Product
    from .serializers import ProductSerializer
    
    # ============================================
    # MODEL TESTS
    # ============================================
    class ProductModelTest(TestCase):
        def setUp(self):
            self.product = Product.objects.create(
                title='Test Product',
                price=99.99
            )
    
        def test_product_creation(self):
            self.assertEqual(self.product.title, 'Test Product')
            self.assertEqual(str(self.product), 'Test Product')
    
        def test_product_price(self):
            self.assertEqual(self.product.price, 99.99)
    
    # ============================================
    # SERIALIZER TESTS
    # ============================================
    class ProductSerializerTest(TestCase):
        def setUp(self):
            self.product = Product.objects.create(
                title='Test Product',
                price=99.99
            )
            self.serializer = ProductSerializer(instance=self.product)
    
        def test_contains_expected_fields(self):
            data = self.serializer.data
            self.assertEqual(set(data.keys()), set(['id', 'title', 'price']))
    
        def test_field_content(self):
            data = self.serializer.data
            self.assertEqual(data['title'], 'Test Product')
            self.assertEqual(float(data['price']), 99.99)
    
        def test_serializer_validation(self):
            invalid_data = {'title': '', 'price': -10}
            serializer = ProductSerializer(data=invalid_data)
            self.assertFalse(serializer.is_valid())
    
    # ============================================
    # API TESTS
    # ============================================
    class ProductAPITest(APITestCase):
        def setUp(self):
            # Create user
            self.user = User.objects.create_user(
                username='testuser',
                password='testpass123'
            )
    
            # Create product
            self.product = Product.objects.create(
                title='Test Product',
                price=99.99,
                owner=self.user
            )
    
            self.client = APIClient()
    
        # Test GET list
        def test_get_product_list(self):
            response = self.client.get('/api/products/')
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.assertEqual(len(response.data['results']), 1)
    
        # Test GET detail
        def test_get_product_detail(self):
            response = self.client.get(f'/api/products/{self.product.id}/')
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.assertEqual(response.data['title'], 'Test Product')
    
        # Test POST (authenticated)
        def test_create_product_authenticated(self):
            self.client.force_authenticate(user=self.user)
            data = {'title': 'New Product', 'price': '49.99'}
            response = self.client.post('/api/products/', data)
            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
            self.assertEqual(Product.objects.count(), 2)
            self.assertEqual(response.data['title'], 'New Product')
    
        # Test POST (unauthenticated)
        def test_create_product_unauthenticated(self):
            data = {'title': 'New Product', 'price': '49.99'}
            response = self.client.post('/api/products/', data)
            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
    
        # Test PUT
        def test_update_product(self):
            self.client.force_authenticate(user=self.user)
            data = {'title': 'Updated Product', 'price': '149.99'}
            response = self.client.put(f'/api/products/{self.product.id}/', data)
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.product.refresh_from_db()
            self.assertEqual(self.product.title, 'Updated Product')
    
        # Test PATCH
        def test_partial_update_product(self):
            self.client.force_authenticate(user=self.user)
            data = {'title': 'Patched Title'}
            response = self.client.patch(f'/api/products/{self.product.id}/', data)
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.product.refresh_from_db()
            self.assertEqual(self.product.title, 'Patched Title')
    
        # Test DELETE
        def test_delete_product(self):
            self.client.force_authenticate(user=self.user)
            response = self.client.delete(f'/api/products/{self.product.id}/')
            self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
            self.assertEqual(Product.objects.count(), 0)
    
        # Test filtering
        def test_filter_products(self):
            Product.objects.create(title='Expensive', price=1000, owner=self.user)
            response = self.client.get('/api/products/?min_price=500')
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.assertEqual(len(response.data['results']), 1)
    
        # Test search
        def test_search_products(self):
            response = self.client.get('/api/products/?search=Test')
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.assertEqual(len(response.data['results']), 1)
    
        # Test pagination
        def test_pagination(self):
            for i in range(15):
                Product.objects.create(
                    title=f'Product {i}',
                    price=10 * i,
                    owner=self.user
                )
            response = self.client.get('/api/products/')
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.assertIn('results', response.data)
            self.assertIn('count', response.data)
    
        # Test permissions
        def test_owner_can_update(self):
            self.client.force_authenticate(user=self.user)
            data = {'title': 'Owner Update'}
            response = self.client.patch(f'/api/products/{self.product.id}/', data)
            self.assertEqual(response.status_code, status.HTTP_200_OK)
    
        def test_non_owner_cannot_update(self):
            other_user = User.objects.create_user(
                username='other',
                password='pass123'
            )
            self.client.force_authenticate(user=other_user)
            data = {'title': 'Unauthorized Update'}
            response = self.client.patch(f'/api/products/{self.product.id}/', data)
            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
    
    # ============================================
    # AUTHENTICATION TESTS
    # ============================================
    class AuthenticationTest(APITestCase):
        def setUp(self):
            self.user = User.objects.create_user(
                username='testuser',
                password='testpass123',
                email='test@example.com'
            )
    
        def test_login_token(self):
            # Test token authentication
            response = self.client.post('/api/token/', {
                'username': 'testuser',
                'password': 'testpass123'
            })
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            self.assertIn('token', response.data)
    
        def test_login_invalid(self):
            response = self.client.post('/api/token/', {
                'username': 'testuser',
                'password': 'wrongpass'
            })
            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
    
    # ============================================
    # RUN TESTS
    # ============================================
    # Run all tests
    python manage.py test
    
    # Run specific app
    python manage.py test myapp
    
    # Run specific test case
    python manage.py test myapp.tests.ProductAPITest
    
    # Run specific test method
    python manage.py test myapp.tests.ProductAPITest.test_get_product_list
    
    # With verbosity
    python manage.py test --verbosity=2
    
    # Keep database
    python manage.py test --keepdb
    
    # Parallel testing
    python manage.py test --parallel
    
    # ============================================
    # COVERAGE
    # ============================================
    # Install
    pip install coverage
    
    # Run with coverage
    coverage run --source='.' manage.py test
    
    # Generate report
    coverage report
    
    # Generate HTML report
    coverage html
    # Open htmlcov/index.html
    
    # Specific app coverage
    coverage run --source='myapp' manage.py test myapp
    Python

    Common Patterns and Snippets

    # ============================================
    # CUSTOM ACTIONS
    # ============================================
    from rest_framework.decorators import action
    
    class ProductViewSet(viewsets.ModelViewSet):
        # Detail action (requires pk)
        @action(detail=True, methods=['post'])
        def like(self, request, pk=None):
            product = self.get_object()
            product.likes += 1
            product.save()
            return Response({'status': 'liked'})
    
        # List action (no pk)
        @action(detail=False, methods=['get'])
        def top_rated(self, request):
            top_products = self.queryset.order_by('-rating')[:10]
            serializer = self.get_serializer(top_products, many=True)
            return Response(serializer.data)
    
        # Multiple methods
        @action(detail=True, methods=['post', 'delete'])
        def favorite(self, request, pk=None):
            if request.method == 'POST':
                # Add to favorites
                return Response({'status': 'added to favorites'})
            else:
                # Remove from favorites
                return Response({'status': 'removed from favorites'})
    
    # ============================================
    # QUERY OPTIMIZATION
    # ============================================
    # Bad - N+1 queries
    products = Product.objects.all()
    for product in products:
        print(product.category.name)  # Database hit for each iteration
    
    # Good - select_related (for ForeignKey and OneToOne)
    products = Product.objects.select_related('category', 'brand')
    
    # Good - prefetch_related (for ManyToMany and reverse ForeignKey)
    products = Product.objects.prefetch_related('tags', 'reviews')
    
    # Combined
    products = Product.objects.select_related('category').prefetch_related('tags')
    
    # In ViewSet
    class ProductViewSet(viewsets.ModelViewSet):
        def get_queryset(self):
            return Product.objects.select_related(
                'category', 'brand'
            ).prefetch_related(
                'tags', 'reviews'
            )
    
    # ============================================
    # CUSTOM QUERYSETS
    # ============================================
    class ProductViewSet(viewsets.ModelViewSet):
        def get_queryset(self):
            queryset = Product.objects.all()
    
            # Filter by user
            if not self.request.user.is_staff:
                queryset = queryset.filter(is_active=True)
    
            # Filter by query params
            category = self.request.query_params.get('category')
            if category:
                queryset = queryset.filter(category__name=category)
    
            # Annotate
            from django.db.models import Count, Avg
            queryset = queryset.annotate(
                review_count=Count('reviews'),
                avg_rating=Avg('reviews__rating')
            )
    
            return queryset
    
    # ============================================
    # FILE UPLOADS
    # ============================================
    # models.py
    class Product(models.Model):
        image = models.ImageField(upload_to='products/')
        document = models.FileField(upload_to='documents/')
    
    # serializers.py
    class ProductSerializer(serializers.ModelSerializer):
        image = serializers.ImageField(required=False)
        image_url = serializers.SerializerMethodField()
    
        def get_image_url(self, obj):
            request = self.context.get('request')
            if obj.image and request:
                return request.build_absolute_uri(obj.image.url)
            return None
    
    # views.py
    class ProductViewSet(viewsets.ModelViewSet):
        def create(self, request, *args, **kwargs):
            serializer = self.get_serializer(data=request.data)
            serializer.is_valid(raise_exception=True)
            self.perform_create(serializer)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
    
    # Upload file
    # POST /api/products/
    # Content-Type: multipart/form-data
    # Body: form-data with 'image' field
    
    # ============================================
    # CUSTOM ERROR HANDLING
    # ============================================
    from rest_framework.views import exception_handler
    from rest_framework.response import Response
    
    def custom_exception_handler(exc, context):
        response = exception_handler(exc, context)
    
        if response is not None:
            custom_response = {
                'error': True,
                'message': str(exc),
                'detail': response.data
            }
            response.data = custom_response
    
        return response
    
    # settings.py
    REST_FRAMEWORK = {
        'EXCEPTION_HANDLER': 'myapp.utils.custom_exception_handler'
    }
    
    # ============================================
    # SIGNALS
    # ============================================
    from django.db.models.signals import post_save
    from django.dispatch import receiver
    from rest_framework.authtoken.models import Token
    
    @receiver(post_save, sender=User)
    def create_auth_token(sender, instance=None, created=False, **kwargs):
        if created:
            Token.objects.create(user=instance)
    
    # ============================================
    # CUSTOM MIDDLEWARE
    # ============================================
    class CustomHeaderMiddleware:
        def __init__(self, get_response):
            self.get_response = get_response
    
        def __call__(self, request):
            response = self.get_response(request)
            response['X-Custom-Header'] = 'My Custom Value'
            return response
    
    # settings.py
    MIDDLEWARE = [
        # ...
        'myapp.middleware.CustomHeaderMiddleware',
    ]
    
    # ============================================
    # BULK OPERATIONS
    # ============================================
    @api_view(['POST'])
    def bulk_create(request):
        serializer = ProductSerializer(data=request.data, many=True)
        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)
    
    @api_view(['PATCH'])
    def bulk_update(request):
        ids = request.data.get('ids', [])
        update_data = request.data.get('data', {})
        Product.objects.filter(id__in=ids).update(**update_data)
        return Response({'updated': len(ids)})
    
    # ============================================
    # CACHING
    # ============================================
    from django.core.cache import cache
    from django.views.decorators.cache import cache_page
    from django.utils.decorators import method_decorator
    
    class ProductViewSet(viewsets.ModelViewSet):
        @method_decorator(cache_page(60 * 15))  # 15 minutes
        def list(self, request, *args, **kwargs):
            return super().list(request, *args, **kwargs)
    
        def perform_create(self, serializer):
            serializer.save()
            cache.delete('product_list')  # Invalidate cache
    
    # Manual caching
    def get_products():
        products = cache.get('all_products')
        if products is None:
            products = list(Product.objects.all())
            cache.set('all_products', products, 60 * 60)  # 1 hour
        return products
    Python

    HTTP Status Codes Reference

    from rest_framework import status
    
    # 2xx Success
    status.HTTP_200_OK                    # GET, PUT, PATCH success
    status.HTTP_201_CREATED               # POST success
    status.HTTP_202_ACCEPTED              # Request accepted, processing
    status.HTTP_204_NO_CONTENT            # DELETE success
    
    # 3xx Redirection
    status.HTTP_301_MOVED_PERMANENTLY
    status.HTTP_302_FOUND
    status.HTTP_304_NOT_MODIFIED
    
    # 4xx Client Errors
    status.HTTP_400_BAD_REQUEST           # Invalid request data
    status.HTTP_401_UNAUTHORIZED          # Not authenticated
    status.HTTP_403_FORBIDDEN             # No permission
    status.HTTP_404_NOT_FOUND             # Resource not found
    status.HTTP_405_METHOD_NOT_ALLOWED    # Invalid HTTP method
    status.HTTP_409_CONFLICT              # Conflict (e.g., duplicate)
    status.HTTP_422_UNPROCESSABLE_ENTITY  # Validation error
    status.HTTP_429_TOO_MANY_REQUESTS     # Rate limit exceeded
    
    # 5xx Server Errors
    status.HTTP_500_INTERNAL_SERVER_ERROR
    status.HTTP_501_NOT_IMPLEMENTED
    status.HTTP_503_SERVICE_UNAVAILABLE
    HTTP

    Quick Reference: Common Commands

    # Project Setup
    django-admin startproject myproject
    python manage.py startapp myapp
    python manage.py runserver
    python manage.py runserver 0.0.0.0:8000
    
    # Database
    python manage.py makemigrations
    python manage.py migrate
    python manage.py showmigrations
    python manage.py sqlmigrate myapp 0001
    python manage.py dbshell
    
    # Users
    python manage.py createsuperuser
    python manage.py changepassword username
    
    # Shell
    python manage.py shell
    python manage.py shell_plus  # django-extensions
    
    # Static Files
    python manage.py collectstatic
    python manage.py findstatic filename
    
    # Testing
    python manage.py test
    python manage.py test myapp
    python manage.py test --keepdb
    python manage.py test --parallel
    
    # Utilities
    python manage.py check
    python manage.py check --deploy
    python manage.py showurls  # django-extensions
    python manage.py show_urls  # django-extensions
    
    # Custom Commands
    python manage.py <custom_command>
    Bash

    Environment Variables Template

    # .env file
    
    # Django
    SECRET_KEY=your-secret-key-here
    DEBUG=True
    ALLOWED_HOSTS=localhost,127.0.0.1
    
    # Database
    DB_ENGINE=django.db.backends.postgresql
    DB_NAME=mydb
    DB_USER=myuser
    DB_PASSWORD=mypassword
    DB_HOST=localhost
    DB_PORT=5432
    
    # Email
    EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
    EMAIL_HOST=smtp.gmail.com
    EMAIL_PORT=587
    EMAIL_USE_TLS=True
    EMAIL_HOST_USER=your-email@gmail.com
    EMAIL_HOST_PASSWORD=your-password
    
    # Redis
    REDIS_URL=redis://localhost:6379/1
    
    # AWS S3
    AWS_ACCESS_KEY_ID=your-access-key
    AWS_SECRET_ACCESS_KEY=your-secret-key
    AWS_STORAGE_BUCKET_NAME=your-bucket-name
    
    # Other
    API_KEY=your-api-key
    Bash

    Happy coding! 🚀


    Discover more from Altgr Blog

    Subscribe to get the latest posts sent to your email.

    Leave a Reply

    Your email address will not be published. Required fields are marked *