Структура проекта, обработка ошибок, логирование, безопасность
Проверенные подходы и паттерны для создания надёжных, безопасных и поддерживаемых API на DRF.
myproject/
├── manage.py
├── config/ # Настройки проекта
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── apps/ # Приложения
│ ├── articles/
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── serializers.py
│ │ ├── views.py
│ │ ├── urls.py
│ │ ├── permissions.py
│ │ ├── filters.py
│ │ └── services.py
│ └── users/
├── tests/ # Тесты
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_articles.py
│ └── test_users.py
├── requirements/
│ ├── base.txt
│ ├── dev.txt
│ └── prod.txt
└── scripts/
└── deploy.sh
articles, users, orders, paymentsapps/core/, apps/common/apps/api/v1/, apps/api/v2/# serializers.py
class ArticleListSerializer(serializers.ModelSerializer):
"""Для списка — минимум полей"""
author_name = serializers.CharField(source='author.username', read_only=True)
class Meta:
model = Article
fields = ['id', 'title', 'author_name', 'created_at']
class ArticleDetailSerializer(serializers.ModelSerializer):
"""Для детального просмотра — все поля"""
author = AuthorSerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
comment_count = serializers.IntegerField(read_only=True)
class Meta:
model = Article
fields = [
'id', 'title', 'content', 'author', 'tags',
'comment_count', 'created_at', 'updated_at'
]
class ArticleCreateSerializer(serializers.ModelSerializer):
"""Для создания — только writable поля"""
class Meta:
model = Article
fields = ['title', 'content', 'tag_ids']
def create(self, validated_data):
tag_ids = validated_data.pop('tag_ids', [])
article = Article.objects.create(**validated_data)
article.tags.set(tag_ids)
return article# models.py
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
class Meta:
constraints = [
models.CheckConstraint(check=models.Q(price__gte=0), name='price_positive'),
models.UniqueConstraint(fields=['title', 'author'], name='unique_title_per_author'),
]
# serializers.py
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['id', 'title', 'content', 'price']
# Валидация constraints будет выполнена автоматически# views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class ArticleViewSet(viewsets.ModelViewSet):
"""
ViewSet для статей.
list: GET /articles/
create: POST /articles/
retrieve: GET /articles/{id}/
update: PUT /articles/{id}/
destroy: DELETE /articles/{id}/
"""
permission_classes = [IsAuthenticatedOrReadOnly]
def get_queryset(self):
return Article.objects.select_related('author').prefetch_related('tags')
def get_serializer_class(self):
if self.action == 'list':
return ArticleListSerializer
elif self.action == 'retrieve':
return ArticleDetailSerializer
return ArticleCreateSerializer
def perform_create(self, serializer):
serializer.save(author=self.request.user)from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
class ArticleViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'], permission_classes=[IsAdminUser])
def publish(self, request, pk=None):
"""Публикация статьи (только админ)"""
article = self.get_object()
article.is_published = True
article.save()
return Response({'status': 'published'})
@action(detail=True, methods=['post'])
def like(self, request, pk=None):
"""Лайк статьи"""
article = self.get_object()
article.likes.add(request.user)
return Response({'status': 'liked'})
@action(detail=False, methods=['get'])
def my_articles(self, request):
"""Мои статьи"""
queryset = Article.objects.filter(author=request.user)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)# utils/exceptions.py
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:
# Единый формат ошибок
error_data = {
'success': False,
'error': {
'code': response.status_code,
'message': response.data.get('detail', 'An error occurred'),
'fields': {}
}
}
# Собираем ошибки по полям
for key, value in response.data.items():
if key != 'detail':
if isinstance(value, list):
error_data['error']['fields'][key] = value[0]
else:
error_data['error']['message'] = value
response.data = error_data
return response
# settings.py
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'myproject.utils.exceptions.custom_exception_handler',
}import logging
logger = logging.getLogger(__name__)
class ArticleViewSet(viewsets.ModelViewSet):
def create(self, request, *args, **kwargs):
try:
return super().create(request, *args, **kwargs)
except Exception as e:
logger.exception(f'Error creating article: {e}')
raise# settings.py
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000 # 1 год
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True# settings.py
INSTALLED_APPS = ['corsheaders']
MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware'] + MIDDLEWARE
CORS_ALLOWED_ORIGINS = [
'https://example.com',
'https://www.example.com',
]
# Или для development
CORS_ALLOW_ALL_ORIGINS = True # Только для dev!# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES': {
'auth': '5/hour', # Защита от брутфорса
'anon': '100/day',
'user': '1000/day',
}
}class UserRegistrationSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
password = serializers.CharField(
min_length=8,
write_only=True,
required=True
)
password_confirm = serializers.CharField(write_only=True, required=True)
def validate(self, data):
if data['password'] != data['password_confirm']:
raise serializers.ValidationError('Passwords do not match')
# Проверка сложности пароля
if not any(c.isupper() for c in data['password']):
raise serializers.ValidationError('Password must contain uppercase letter')
return data# tests/conftest.py
import pytest
from django.contrib.auth.models import User
@pytest.fixture
def user():
return User.objects.create_user(username='testuser', password='testpass')
@pytest.fixture
def authenticated_client(client, user):
client.force_authenticate(user=user)
return client
@pytest.fixture
def article(user):
return Article.objects.create(
title='Test Article',
content='Test content',
author=user
)# tests/test_articles.py
import pytest
from rest_framework import status
@pytest.mark.django_db
class TestArticleViewSet:
def test_list_articles(self, client, article):
response = client.get('/api/articles/')
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 1
def test_create_article_unauthenticated(self, client):
response = client.post('/api/articles/', {})
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_create_article_authenticated(self, authenticated_client):
data = {'title': 'New Article', 'content': 'Content'}
response = authenticated_client.post('/api/articles/', data)
assert response.status_code == status.HTTP_201_CREATED
assert Article.objects.count() == 2
def test_delete_not_own_article(self, authenticated_client, article):
# Пользователь не может удалить чужую статью
response = authenticated_client.delete(f'/api/articles/{article.id}/')
assert response.status_code == status.HTTP_403_FORBIDDENclass ArticleViewSet(viewsets.ModelViewSet):
"""
ViewSet для управления статьями.
list:
Возвращает список всех статей.
Поддерживает фильтрацию, поиск и пагинацию.
create:
Создаёт новую статью.
Требуется аутентификация.
retrieve:
Возвращает статью по ID.
Включает информацию об авторе и тегах.
update:
Полное обновление статьи.
Только владелец или админ.
partial_update:
Частичное обновление статьи.
Только владелец или админ.
destroy:
Удаляет статью.
Только владелец или админ.
"""
pass# settings.py
LOGGING = {
'version': 1,
'formatters': {
'json': {
'()': 'pythonjsonlogger.jsonlogger.JsonFormatter',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'json',
},
},
'loggers': {
'django': {
'handlers': ['console'],
'level': 'INFO',
},
'myproject': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': False,
},
},
}
# В коде
import logging
logger = logging.getLogger(__name__)
logger.info('Article created', extra={
'article_id': article.id,
'author_id': article.author_id,
})DEBUG = TrueBrowsableAPIRendererВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.