Chapter 14: API Development
14.2 RESTful API Design
API Version Control
Implement API version control to ensure backward compatibility:
python
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
# Version 1 routes
v1_router = DefaultRouter()
v1_router.register(r'articles', views.v1.ArticleViewSet, basename='v1-article')
v1_router.register(r'categories', views.v1.CategoryViewSet, basename='v1-category')
# Version 2 routes
v2_router = DefaultRouter()
v2_router.register(r'articles', views.v2.ArticleViewSet, basename='v2-article')
v2_router.register(r'categories', views.v2.CategoryViewSet, basename='v2-category')
v2_router.register(r'tags', views.v2.TagViewSet, basename='v2-tag')
urlpatterns = [
# Version 1 API
path('api/v1/', include(v1_router.urls)),
path('api/v1/', include('myapp.api.v1.urls')),
# Version 2 API
path('api/v2/', include(v2_router.urls)),
path('api/v2/', include('myapp.api.v2.urls')),
# Default version (points to latest version)
path('api/', include(v2_router.urls)),
]
# Version control through Accept header
# settings.py
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
'DEFAULT_VERSION': 'v2',
'ALLOWED_VERSIONS': ['v1', 'v2'],
}
# Or version control through URL path
# urls.py
from rest_framework.versioning import URLPathVersioning
urlpatterns = [
path('api/<str:version>/', include(router.urls)),
]
# Handle version in views
class ArticleViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.request.version == 'v1':
return ArticleSerializerV1
return ArticleSerializerV2
def get_queryset(self):
queryset = Article.objects.all()
if self.request.version == 'v1':
# Special handling for v1 version
queryset = queryset.filter(status='published')
return querysetAuthentication and Permissions
Implement multiple authentication and permission control mechanisms:
python
# authentication.py
from rest_framework.authentication import TokenAuthentication, SessionAuthentication
from rest_framework.permissions import BasePermission, IsAuthenticated, IsAdminUser
from rest_framework.authtoken.models import Token
class CustomTokenAuthentication(TokenAuthentication):
"""Custom Token authentication"""
def authenticate_credentials(self, key):
try:
token = Token.objects.select_related('user').get(key=key)
except Token.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid Token')
if not token.user.is_active:
raise exceptions.AuthenticationFailed('User account has been disabled')
# Check if Token has expired
from django.utils import timezone
from datetime import timedelta
if token.created < timezone.now() - timedelta(days=30):
raise exceptions.AuthenticationFailed('Token has expired')
return (token.user, token)
# permissions.py
from rest_framework.permissions import BasePermission
class IsOwnerOrReadOnly(BasePermission):
"""Only allow owners to edit objects"""
def has_object_permission(self, request, view, obj):
# Read permissions allow any request
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions only allow the owner
return obj.author == request.user
class IsAuthorOrAdmin(BasePermission):
"""Only allow authors or administrators"""
def has_permission(self, request, view):
# Anonymous users can only view
if request.method in permissions.SAFE_METHODS:
return True
# Authenticated users can create
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
# Read permissions allow any request
if request.method in permissions.SAFE_METHODS:
return True
# Modify permissions only allow authors or administrators
return obj.author == request.user or request.user.is_staff
class IsVerifiedUser(BasePermission):
"""Only allow verified users"""
def has_permission(self, request, view):
return request.user and request.user.is_authenticated and request.user.profile.is_verified
# views.py
from rest_framework.decorators import action
from rest_framework.response import Response
class ArticleViewSet(viewsets.ModelViewSet):
authentication_classes = [CustomTokenAuthentication, SessionAuthentication]
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
@action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
def like(self, request, pk=None):
"""Like article"""
article = self.get_object()
# Implement like logic
article.likes.add(request.user)
return Response({'status': 'liked'})
@action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
def unlike(self, request, pk=None):
"""Unlike article"""
article = self.get_object()
article.likes.remove(request.user)
return Response({'status': 'unliked'})
@action(detail=False, methods=['get'], permission_classes=[IsAdminUser])
def draft_articles(self, request):
"""Get draft articles (admin only)"""
drafts = Article.objects.filter(status='draft')
serializer = self.get_serializer(drafts, many=True)
return Response(serializer.data)Pagination and Filtering
Implement pagination and filtering functions:
python
# pagination.py
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination
from rest_framework.response import Response
class CustomPageNumberPagination(PageNumberPagination):
"""Custom page number pagination"""
page_size = 20
page_size_query_param = 'page_size'
max_page_size = 100
def get_paginated_response(self, data):
return Response({
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link()
},
'count': self.page.paginator.count,
'total_pages': self.page.paginator.num_pages,
'current_page': self.page.number,
'results': data
})
class CursorPagination(CursorPagination):
"""Cursor pagination"""
page_size = 20
ordering = '-created_at'
# filters.py
from django_filters import rest_framework as filters
from .models import Article, Category
class ArticleFilter(filters.FilterSet):
"""Article filter"""
title = filters.CharFilter(lookup_expr='icontains')
category = filters.ModelChoiceFilter(queryset=Category.objects.all())
tags = filters.ModelMultipleChoiceFilter(
queryset=Tag.objects.all(),
field_name='tags',
lookup_expr='in'
)
author = filters.CharFilter(field_name='author__username', lookup_expr='icontains')
created_after = filters.DateFilter(field_name='created_at', lookup_expr='gte')
created_before = filters.DateFilter(field_name='created_at', lookup_expr='lte')
is_published = filters.BooleanFilter(field_name='status', method='filter_is_published')
class Meta:
model = Article
fields = {
'title': ['icontains'],
'category': ['exact'],
'author': ['exact'],
'created_at': ['gte', 'lte'],
}
def filter_is_published(self, queryset, name, value):
status = 'published' if value else 'draft'
return queryset.filter(status=status)
# views.py
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter, OrderingFilter
class ArticleViewSet(viewsets.ModelViewSet):
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = ArticleFilter
search_fields = ['title', 'content', 'excerpt', 'author__username']
ordering_fields = ['created_at', 'updated_at', 'view_count', 'published_at']
ordering = ['-created_at']
pagination_class = CustomPageNumberPagination
@action(detail=False, methods=['get'])
def popular(self, request):
"""Get popular articles"""
queryset = self.filter_queryset(
self.get_queryset().filter(view_count__gt=1000)
)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)API Documentation Generation
Generate API documentation:
python
# settings.py
INSTALLED_APPS = [
# ...
'rest_framework',
'rest_framework.authtoken',
'drf_yasg', # Swagger/OpenAPI documentation
]
# urls.py
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
schema_view = get_schema_view(
openapi.Info(
title="Blog API",
default_version='v2',
description="RESTful API documentation for the blog system",
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="contact@blog.local"),
license=openapi.License(name="BSD License"),
),
public=True,
permission_classes=[permissions.AllowAny],
)
urlpatterns = [
# API routes
path('api/v2/', include(router.urls)),
# API documentation
path('swagger/', schema_view.with_ui(
'swagger',
cache_timeout=0
), name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui(
'redoc',
cache_timeout=0
), name='schema-redoc'),
path('swagger.json', schema_view.without_ui(
cache_timeout=0
), name='schema-json'),
]
# serializers.py - Add documentation comments
class ArticleSerializer(serializers.ModelSerializer):
"""
Article serializer
Used to serialize and deserialize Article model instances
"""
class Meta:
model = Article
fields = '__all__'
read_only_fields = ['author', 'view_count', 'created_at', 'updated_at']
def create(self, validated_data):
"""
Create article
Args:
validated_data (dict): Validated data
Returns:
Article: Created article instance
"""
return Article.objects.create(**validated_data)
# views.py - Add detailed documentation
class ArticleViewSet(viewsets.ModelViewSet):
"""
Article ViewSet
Provides complete CRUD operations for articles
"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
@action(detail=True, methods=['post'])
def like(self, request, pk=None):
"""
Like article
Add like to the specified article
Args:
request: HTTP request object
pk (int): Article ID
Returns:
Response: Like result
"""
article = self.get_object()
article.likes.add(request.user)
return Response({'status': 'liked'})Through these RESTful API design practices, you can create feature-complete, easy-to-use, and maintainable API interfaces.
Summary
Core points of RESTful API design:
- ✅ Implement API version control to ensure backward compatibility
- ✅ Design flexible authentication and permission control mechanisms
- ✅ Implement efficient pagination and filtering functions
- ✅ Generate complete API documentation for developer use
- ✅ Follow REST architectural constraints and best practices
Good API design can improve development efficiency and user experience.
Next Article
We will learn about the Django testing system.