Table of Contents
- Introduction
- Prerequisites
- Environment Setup
- Creating a Django Project
- Installing Django REST Framework
- Building Your First API
- Models and Serializers
- ViewSets and Routers
- Authentication and Permissions
- Filtering, Searching, and Pagination
- Testing Your API
- Advanced Features
- Deployment
- 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_apiBash2. Create a Virtual Environment
# Windows
python -m venv venv
venv\Scripts\activate
# macOS/Linux
python3 -m venv venv
source venv/bin/activateBash3. Install Required Packages
pip install django djangorestframework
pip install django-filter markdown
pip install djangorestframework-simplejwt
pip install django-cors-headersBash4. Create requirements.txt
pip freeze > requirements.txtBashCreating a Django Project
1. Start a New Django Project
django-admin startproject bookapi .Bash2. Create an App
python manage.py startapp booksBash3. 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.txtBashInstalling 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",
]Python2. Run Initial Migrations
python manage.py migratePythonBuilding 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']),
]Python2. Create and Run Migrations
python manage.py makemigrations
python manage.py migrateBash3. 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'Python4. Create a Superuser
python manage.py createsuperuserBashModels 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']Python2. 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)Python2. 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_staffPython3. 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)),
]Python4. 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'),
]PythonAuthentication 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'))PythonRun it:
python manage.py create_tokensBash2. JWT Authentication (Alternative)
Install package:
pip install djangorestframework-simplejwtBashUpdate 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,
}PythonAdd 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'),
]Python3. 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
)PythonAdd to URLs:
# books/urls.py
from .views import UserRegistrationView
urlpatterns = [
path('', include(router.urls)),
path('register/', UserRegistrationView.as_view(), name='register'),
]Python4. 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 IsAdminUserPythonFiltering, 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_fieldsPython2. 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 = 100PythonApply to ViewSet:
# books/views.py
from .pagination import BookPagination
class BookViewSet(viewsets.ModelViewSet):
# ... existing code
pagination_class = BookPaginationPython3. 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=1ANSITesting 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)Python2. 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 --keepdbBash3. Coverage Report
pip install coverage
# Run tests with coverage
coverage run --source='.' manage.py test
# Generate report
coverage report
# Generate HTML report
coverage htmlBashAdvanced 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'
}
}PythonCustom 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'PythonApply to ViewSet:
from .throttles import BurstRateThrottle, SustainedRateThrottle
class BookViewSet(viewsets.ModelViewSet):
throttle_classes = [BurstRateThrottle, SustainedRateThrottle]Python2. API Versioning
# bookapi/settings.py
REST_FRAMEWORK = {
# ... existing settings
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
'DEFAULT_VERSION': 'v1',
'ALLOWED_VERSIONS': ['v1', 'v2'],
}PythonURLs:
# bookapi/urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('books.urls')),
path('api/v2/', include('books.urls_v2')), # Different version
]PythonVersion-specific logic:
class BookViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.request.version == 'v2':
return BookDetailSerializerV2
return BookDetailSerializerPython3. 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)Python4. 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 NonePython5. 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__'Python6. 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'})Python7. 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()})Python8. API Documentation with drf-spectacular
pip install drf-spectacularBash# 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'),
]PythonDeployment
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'Python2. 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=5432PythonInstall python-decouple:
pip install python-decoupleBashUse in settings:
from decouple import config
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)Python3. 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"]Dockerfiledocker-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:Dockerfile4. Gunicorn Configuration
pip install gunicornBashgunicorn_config.py:
bind = "0.0.0.0:8000"
workers = 4
worker_class = "sync"
worker_connections = 1000
keepalive = 5
errorlog = "-"
accesslog = "-"
loglevel = "info"Bash5. 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/;
}
}Nginx6. 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 webBashBest 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.pyBash2. 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 queryPython3. 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 securityPython4. 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'
]
}Python5. 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 booksPython6. 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)Python7. 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=1PythonResponse Format:
# Consistent response structure
{
"status": "success",
"data": {...},
"message": "Book created successfully"
}
# Error responses
{
"status": "error",
"errors": {
"field": ["Error message"]
},
"message": "Validation failed"
}Python8. 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.
"""
passPythonConclusion
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
- Practice: Build your own API project
- Explore: Django Channels for WebSockets
- Learn: GraphQL with Graphene-Django
- Study: Microservices architecture
- Contribute: Open-source Django projects
Useful Resources
- Django REST Framework Documentation
- Django Documentation
- Classy Django REST Framework
- Django REST Framework Tutorial
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-extensionsBashDjango 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:applicationBashSettings 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,
}PythonModels 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')
]PythonSerializers 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 authorPythonViews 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)PythonURL 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()),
]PythonAuthentication 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)PythonPermissions 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):
passPythonFiltering, 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=2PythonTesting 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 myappPythonCommon 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 productsPythonHTTP 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_UNAVAILABLEHTTPQuick 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>BashEnvironment 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-keyBashHappy coding! 🚀
Discover more from Altgr Blog
Subscribe to get the latest posts sent to your email.
