Версионирование Django API
Версионирование позволяет развивать API без нарушения работы существующих клиентов. Это критически важно для production API, которые используют множество клиентов.
Настройка версионирования
Настрой версионирование в settings.py:
URL версионирование
Самый простой и понятный способ:
1# project/urls.py
2from django.urls import path, include
3
4urlpatterns = [
5 path('api/v1/', include('myapp.urls.v1')),
6 path('api/v2/', include('myapp.urls.v2')),
7 path('api/v3/', include('myapp.urls.v3')),
8]
9
10# myapp/urls/v1.py
11from django.urls import path
12from .views import BookViewSetV1
13
14urlpatterns = [
15 path('books/', BookViewSetV1.as_view({'get': 'list', 'post': 'create'})),
16 path('books/<int:pk>/', BookViewSetV1.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
17]
18
19# myapp/urls/v2.py
20from django.urls import path
21from .views import BookViewSetV2
22
23urlpatterns = [
24 path('books/', BookViewSetV2.as_view({'get': 'list', 'post': 'create'})),
25 path('books/<int:pk>/', BookViewSetV2.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
26 path('books/<int:pk>/reviews/', BookViewSetV2.as_view({'get': 'reviews'})),
27]
Header версионирование
Версионирование через HTTP заголовки:
1REST_FRAMEWORK = {
2 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
3 'DEFAULT_VERSION': 'v1',
4 'ALLOWED_VERSIONS': ['v1', 'v2'],
5}
6
7# Клиент отправляет заголовок:
8# Accept: application/json; version=v2
9
10# В view можно получить версию:
11def get(self, request, *args, **kwargs):
12 version = request.version
13 if version == 'v1':
14 return self.v1_response()
15 elif version == 'v2':
16 return self.v2_response()
17 return self.default_response()
Namespace версионирование
Версионирование через пространства имен:
1REST_FRAMEWORK = {
2 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
3 'DEFAULT_VERSION': 'v1',
4 'ALLOWED_VERSIONS': ['v1', 'v2'],
5}
6
7# urls.py
8urlpatterns = [
9 path('api/', include(('myapp.urls.v1', 'v1'), namespace='v1')),
10 path('api/', include(('myapp.urls.v2', 'v2'), namespace='v2')),
11]
Версионирование сериализаторов
Создавай разные сериализаторы для разных версий:
1# serializers.py
2from rest_framework import serializers
3from .models import Book
4
5class BookSerializerV1(serializers.ModelSerializer):
6 class Meta:
7 model = Book
8 fields = ['id', 'title', 'author', 'price']
9
10class BookSerializerV2(serializers.ModelSerializer):
11 author_name = serializers.CharField(source='author.name', read_only=True)
12 category = serializers.CharField()
13 published_date = serializers.DateField()
14
15 class Meta:
16 model = Book
17 fields = ['id', 'title', 'author', 'author_name', 'price', 'category', 'published_date']
18
19class BookSerializerV3(BookSerializerV2):
20 reviews_count = serializers.SerializerMethodField()
21 average_rating = serializers.SerializerMethodField()
22
23 def get_reviews_count(self, obj):
24 return obj.reviews.count()
25
26 def get_average_rating(self, obj):
27 reviews = obj.reviews.all()
28 if reviews:
29 return sum(review.rating for review in reviews) / len(reviews)
30 return 0
31
32 class Meta(BookSerializerV2.Meta):
33 fields = BookSerializerV2.Meta.fields + ['reviews_count', 'average_rating']
Версионирование views
Создавай разные views для разных версий:
1# views.py
2from rest_framework import viewsets
3from rest_framework.decorators import action
4from rest_framework.response import Response
5from .models import Book
6from .serializers import BookSerializerV1, BookSerializerV2, BookSerializerV3
7
8class BookViewSetV1(viewsets.ModelViewSet):
9 queryset = Book.objects.all()
10 serializer_class = BookSerializerV1
11
12 def get_queryset(self):
13 return Book.objects.select_related('author')
14
15class BookViewSetV2(BookViewSetV1):
16 serializer_class = BookSerializerV2
17
18 def get_queryset(self):
19 return Book.objects.select_related('author').prefetch_related('reviews')
20
21 @action(detail=True, methods=['get'])
22 def reviews(self, request, pk=None):
23 book = self.get_object()
24 reviews = book.reviews.all()
25 return Response({'reviews': [{'id': r.id, 'rating': r.rating} for r in reviews]})
26
27class BookViewSetV3(BookViewSetV2):
28 serializer_class = BookSerializerV3
29
30 def get_queryset(self):
31 return Book.objects.select_related('author').prefetch_related('reviews', 'category')
32
33 @action(detail=True, methods=['get'])
34 def statistics(self, request, pk=None):
35 book = self.get_object()
36 return Response({
37 'total_reviews': book.reviews.count(),
38 'average_rating': book.reviews.aggregate(Avg('rating'))['rating__avg'],
39 'last_review': book.reviews.last().created_at if book.reviews.exists() else None
40 })
Условная логика в views
Обрабатывай разные версии в одном view:
1class BookViewSet(viewsets.ModelViewSet):
2 queryset = Book.objects.all()
3
4 def get_serializer_class(self):
5 version = self.request.version
6
7 if version == 'v1':
8 return BookSerializerV1
9 elif version == 'v2':
10 return BookSerializerV2
11 elif version == 'v3':
12 return BookSerializerV3
13 else:
14 return BookSerializerV1 # По умолчанию
15
16 def get_queryset(self):
17 version = self.request.version
18 queryset = Book.objects.all()
19
20 if version in ['v2', 'v3']:
21 queryset = queryset.select_related('author').prefetch_related('reviews')
22
23 if version == 'v3':
24 queryset = queryset.prefetch_related('category')
25
26 return queryset
27
28 def list(self, request, *args, **kwargs):
29 response = super().list(request, *args, **kwargs)
30
31 # Добавляем метаданные в зависимости от версии
32 if request.version == 'v3':
33 response.data['meta'] = {
34 'total_count': self.get_queryset().count(),
35 'version': 'v3',
36 'features': ['reviews', 'statistics', 'category']
37 }
38
39 return response
Версионирование permissions
Разные права доступа для разных версий:
1from rest_framework import permissions
2
3class VersionBasedPermission(permissions.BasePermission):
4 def has_permission(self, request, view):
5 version = request.version
6
7 # v1 - только чтение
8 if version == 'v1':
9 return request.method in permissions.SAFE_METHODS
10
11 # v2 - чтение и создание
12 elif version == 'v2':
13 return request.method in permissions.SAFE_METHODS + ['POST']
14
15 # v3 - полный доступ
16 elif version == 'v3':
17 return True
18
19 return False
20
21class BookViewSet(viewsets.ModelViewSet):
22 permission_classes = [VersionBasedPermission]
23 # ... остальной код
Версионирование фильтрации
Разные фильтры для разных версий:
1from django_filters import rest_framework as filters
2
3class BookFilterV1(filters.FilterSet):
4 title = filters.CharFilter(lookup_expr='icontains')
5 author = filters.CharFilter(lookup_expr='icontains')
6
7 class Meta:
8 model = Book
9 fields = ['title', 'author']
10
11class BookFilterV2(BookFilterV1):
12 price_min = filters.NumberFilter(field_name='price', lookup_expr='gte')
13 price_max = filters.NumberFilter(field_name='price', lookup_expr='lte')
14
15 class Meta(BookFilterV1.Meta):
16 fields = BookFilterV1.Meta.fields + ['price_min', 'price_max']
17
18class BookFilterV3(BookFilterV2):
19 category = filters.CharFilter()
20 published_after = filters.DateFilter(field_name='published_date', lookup_expr='gte')
21
22 class Meta(BookFilterV2.Meta):
23 fields = BookFilterV2.Meta.fields + ['category', 'published_after']
24
25class BookViewSet(viewsets.ModelViewSet):
26 def get_filterset_class(self):
27 version = self.request.version
28
29 if version == 'v1':
30 return BookFilterV1
31 elif version == 'v2':
32 return BookFilterV2
33 elif version == 'v3':
34 return BookFilterV3
35 else:
36 return BookFilterV1
Версионирование пагинации
Разные настройки пагинации:
1from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination
2
3class BookPaginationV1(PageNumberPagination):
4 page_size = 20
5 page_size_query_param = 'page_size'
6 max_page_size = 100
7
8class BookPaginationV2(LimitOffsetPagination):
9 default_limit = 25
10 limit_query_param = 'limit'
11 offset_query_param = 'offset'
12 max_limit = 200
13
14class BookPaginationV3(PageNumberPagination):
15 page_size = 50
16 page_size_query_param = 'page_size'
17 max_page_size = 500
18
19class BookViewSet(viewsets.ModelViewSet):
20 def get_pagination_class(self):
21 version = self.request.version
22
23 if version == 'v1':
24 return BookPaginationV1
25 elif version == 'v2':
26 return BookPaginationV2
27 elif version == 'v3':
28 return BookPaginationV3
29 else:
30 return BookPaginationV1
Обратная совместимость
Обеспечивай совместимость между версиями:
1class BookSerializerV2(serializers.ModelSerializer):
2 # Новые поля
3 category = serializers.CharField()
4 published_date = serializers.DateField()
5
6 # Поля для обратной совместимости
7 title = serializers.CharField()
8 author = serializers.CharField(source='author.name')
9
10 def to_representation(self, instance):
11 data = super().to_representation(instance)
12
13 # Добавляем поля по умолчанию для v1 клиентов
14 if not data.get('category'):
15 data['category'] = 'unknown'
16
17 if not data.get('published_date'):
18 data['published_date'] = instance.created_at.date()
19
20 return data
21
22 class Meta:
23 model = Book
24 fields = ['id', 'title', 'author', 'category', 'published_date']
Тестирование версий
Создавай тесты для разных версий:
1from django.test import TestCase
2from django.urls import reverse
3from rest_framework.test import APIClient
4from .models import Book, Author
5
6class BookAPIVersioningTest(TestCase):
7 def setUp(self):
8 self.client = APIClient()
9 self.author = Author.objects.create(name='Test Author')
10 self.book = Book.objects.create(
11 title='Test Book',
12 author=self.author,
13 price=19.99
14 )
15
16 def test_v1_api(self):
17 url = reverse('v1:book-list')
18 response = self.client.get(url)
19
20 self.assertEqual(response.status_code, 200)
21 self.assertIn('title', response.data[0])
22 self.assertNotIn('category', response.data[0]) # v1 не имеет этого поля
23
24 def test_v2_api(self):
25 url = reverse('v2:book-list')
26 response = self.client.get(url)
27
28 self.assertEqual(response.status_code, 200)
29 self.assertIn('category', response.data[0]) # v2 имеет это поле
30
31 def test_v3_api(self):
32 url = reverse('v3:book-list')
33 response = self.client.get(url)
34
35 self.assertEqual(response.status_code, 200)
36 self.assertIn('reviews_count', response.data[0]) # v3 имеет это поле
Документация API
Используй drf-spectacular для автоматической документации:
1# settings.py
2INSTALLED_APPS = [
3 # ... другие приложения
4 'drf_spectacular',
5]
6
7SPECTACULAR_SETTINGS = {
8 'TITLE': 'Book API',
9 'DESCRIPTION': 'API для управления книгами',
10 'VERSION': '1.0.0',
11 'SERVE_INCLUDE_SCHEMA': False,
12 'COMPONENT_SPLIT_REQUEST': True,
13 'SCHEMA_PATH_PREFIX': '/api/',
14}
15
16# urls.py
17from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
18
19urlpatterns = [
20 path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
21 path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
22 # ... версионированные URL
23]
Лучшие практики
- Всегда поддерживай обратную совместимость
- Используй семантическое версионирование (v1, v2, v3)
- Документируй изменения между версиями
- Тестируй все версии API
- Используй URL версионирование для простоты
- Создавай отдельные сериализаторы для каждой версии
- Планируй deprecation старых версий
- Мониторь использование разных версий
- Предоставляй миграционные гайды для клиентов
FAQ
Q: Как обрабатывать разные версии в одном view?
A: Используй request.version для определения версии и условной логики.
Q: Какой способ версионирования лучше?
A: URL версионирование проще для понимания, Header версионирование более гибкое.
Q: Как обеспечить обратную совместимость?
A: Не удаляй поля, только добавляй новые, используй значения по умолчанию.
Q: Когда deprecate старые версии?
A: После того как большинство клиентов перешли на новую версию, обычно через 6-12 месяцев.
Q: Как тестировать разные версии?
A: Создавай отдельные тесты для каждой версии и тестируй совместимость.
Q: Нужно ли версионировать все endpoints?
A: Нет, только те, которые изменяются. Стабильные endpoints могут оставаться без версий.