Skip to content

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 queryset

Authentication 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:

  1. ✅ Implement API version control to ensure backward compatibility
  2. ✅ Design flexible authentication and permission control mechanisms
  3. ✅ Implement efficient pagination and filtering functions
  4. ✅ Generate complete API documentation for developer use
  5. ✅ 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.

15.1 Unit Testing →

Directory

Return to Course Directory

Released under the [BY-NC-ND License](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.en).