Best Practices

This page documents recommended patterns for production drf-commons deployments.

Model Design

Always define explicit QuerySet scope on ViewSets

drf-commons does not automatically exclude soft-deleted records. Define the queryset scope explicitly:

class ArticleViewSet(BaseViewSet):
    # Always explicit — include select_related/prefetch_related for performance
    queryset = (
        Article.objects
        .filter(is_active=True)
        .select_related("created_by", "category")
        .prefetch_related("tags")
    )

Minimize the BaseModelMixin stack when not needed

Not every model needs UUID primary keys, user tracking, and soft deletes. Compose only what you need:

# Full stack: only for main domain objects
class Order(BaseModelMixin): ...

# Timestamps only: for join/through tables
class OrderTag(TimeStampMixin, models.Model): ...

# No drf-commons mixins: for lookup/reference tables
class Country(models.Model): ...

Serializer Design

Use the most restrictive field type that satisfies requirements

Prefer IdToDataField over FlexibleField when the client always sends IDs. FlexibleField’s auto-detection adds overhead and ambiguity.

# Good: specific, predictable
author = IdToDataField(queryset=User.objects.all(), serializer=UserSerializer)

# Use only when client contract genuinely requires flexible input
author = FlexibleField(queryset=User.objects.all(), serializer=UserSerializer)

Avoid deep serializer nesting in list endpoints

Nested serializers in list responses cause N+1 queries unless properly prefetched. For list endpoints, prefer flat representations:

class ArticleListSerializer(BaseModelSerializer):
    author_name = serializers.CharField(source="created_by.get_full_name")

    class Meta:
        model = Article
        fields = ["id", "title", "author_name", "created_at"]

class ArticleDetailSerializer(BaseModelSerializer):
    author = IdToDataField(queryset=User.objects.all(), serializer=UserSerializer)

    class Meta:
        model = Article
        fields = ["id", "title", "content", "author", "created_at", "updated_at"]

class ArticleViewSet(BaseViewSet):
    def get_serializer_class(self):
        if self.action == "list":
            return ArticleListSerializer
        return ArticleDetailSerializer

ViewSet Design

Always specify permission_classes

drf-commons ViewSets do not impose default permission classes. Always be explicit:

class ArticleViewSet(BaseViewSet):
    permission_classes = [IsAuthenticated]  # Always explicit

Use bulk operations for large datasets

For any operation touching more than ~10 records, use bulk endpoints:

# Don't create 500 articles via 500 POST /articles/ calls
# Do: POST /articles/bulk-create/ with list of 500 objects

Define explicit ordering

Always define ordering_fields on ViewSets that use ComputedOrderingFilter to prevent exposure of arbitrary database field ordering:

class ArticleViewSet(BaseViewSet):
    filter_backends = [ComputedOrderingFilter]
    ordering_fields = ["title", "created_at"]  # Explicit allowlist
    ordering = ["-created_at"]                 # Default ordering

Bulk Operations

Tune batch sizes per environment

The default BULK_OPERATION_BATCH_SIZE of 1000 may be too large for memory-constrained deployments or too small for high-throughput systems:

# settings/production.py
COMMON = {
    "BULK_OPERATION_BATCH_SIZE": 500,   # Conservative for memory
}

# settings/batch_worker.py (for dedicated batch processing workers)
COMMON = {
    "BULK_OPERATION_BATCH_SIZE": 5000,
}

Use soft delete for user-controlled deletion

Prefer soft delete for any resource that a user might delete:

class ArticleViewSet(BaseViewSet):
    def perform_destroy(self, instance):
        instance.soft_delete()

    @action(detail=True, methods=["post"])
    def restore(self, request, pk=None):
        article = self.get_object()
        article.restore()
        return success_response(message="Article restored.")

File Import/Export

Validate import configuration at startup

Use ConfigValidator or test the import config in application tests to catch configuration errors before deploying:

class EmployeeImportTest(TestCase):
    def test_import_config_valid(self):
        from drf_commons.services.import_from_file.config import ConfigValidator
        validator = ConfigValidator(EmployeeViewSet.import_file_config)
        validator.validate()  # Raises on invalid config

Restrict file export to authorized users

Export endpoints return potentially sensitive data. Apply permission classes:

class EmployeeViewSet(BaseViewSet):
    permission_classes = [IsAuthenticated]
    export_permission_classes = [IsAdminUser]  # Stricter for export

Use chunk_size for large imports

Any import expected to exceed 1000 rows should use chunk_size:

import_file_config = {
    "chunk_size": 250,
    ...
}

Performance

Prefer ``use_save_on_bulk_update = False`` for pure data updates

When signal handlers are not required, the default bulk_update() mode reduces database round-trips from N to 1:

class ProductPriceViewSet(BulkUpdateViewSet):
    use_save_on_bulk_update = False  # 1 SQL UPDATE for all records

Use ``ReadOnlyViewSet`` for read-heavy resources

Restricting a resource to read-only removes the overhead of write permission checks and response serialization for mutation operations:

class CountryViewSet(ReadOnlyViewSet):
    queryset = Country.objects.all().order_by("name")
    serializer_class = CountrySerializer
    pagination_class = None  # Small, fixed dataset — no pagination needed

Testing

Use drf-commons test factories in all tests

from drf_commons.common_tests.factories import UserFactory, StaffUserFactory

class ArticleViewSetTest(TestCase):
    def setUp(self):
        self.user = UserFactory()
        self.staff = StaffUserFactory()
        self.client = APIClient()
        self.client.force_authenticate(user=self.user)

Set the context user in model-level tests

from drf_commons.current_user.utils import _set_current_user, _reset_current_user

class UserActionMixinTest(TestCase):
    def test_created_by_populated(self):
        user = UserFactory()
        token = _set_current_user(user)
        try:
            article = Article.objects.create(title="Test", content="Body")
            self.assertEqual(article.created_by, user)
        finally:
            _reset_current_user(token)