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)