Homechevron_rightBlogchevron_rightBackend
Backendschedule7 min read10 April 2025

Django REST Framework: Patterns I Use in Production

From custom serializer mixins to Celery task patterns and PostgreSQL query optimisation — the DRF techniques that actually matter when your API handles real traffic.

PythonDjangoPostgreSQLRESTCelery
smart_toy

AI-Assisted Content. This article was generated with AI and reviewed for accuracy based on real engineering experience. Code examples are tested and production-relevant.

Introduction

Django REST Framework is one of the most productive tools in the Python ecosystem. But most tutorials stop at CRUD. Here are the patterns I reach for when building healthcare and agri-tech APIs in production.


1. Custom Serializer Mixins for DRY Validation

class TimestampedSerializer(serializers.ModelSerializer):
    created_at = serializers.DateTimeField(read_only=True, format="%Y-%m-%dT%H:%M:%SZ")
    updated_at = serializers.DateTimeField(read_only=True, format="%Y-%m-%dT%H:%M:%SZ")

class AuditedCreateMixin(serializers.Serializer):
    def create(self, validated_data):
        validated_data['created_by'] = self.context['request'].user
        return super().create(validated_data)

2. select_related / prefetch_related — Always

The N+1 query is the most common performance killer in DRF. Use Django Debug Toolbar in development and annotate your querysets:

class PatientViewSet(viewsets.ModelViewSet):
    def get_queryset(self):
        return (
            Patient.objects
            .select_related('facility', 'assigned_clinician')
            .prefetch_related('appointments__prescriptions')
            .filter(facility=self.request.user.facility)
        )

3. Celery for Background Tasks

Any operation that takes > 200ms doesn't belong in a request/response cycle. Billing, notifications, report generation — all go to Celery:

@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def send_appointment_reminder(self, appointment_id: int):
    try:
        appointment = Appointment.objects.select_related('patient').get(id=appointment_id)
        SMSGateway.send(appointment.patient.phone, f"Reminder: {appointment.time}")
    except Exception as exc:
        raise self.retry(exc=exc)

4. Custom Pagination with Metadata

Standard PageNumberPagination gives you next and previous. Add total counts and page metadata so frontend engineers don't have to guess:

class StandardPagination(PageNumberPagination):
    page_size = 20
    page_size_query_param = 'page_size'

    def get_paginated_response(self, data):
        return Response({
            'meta': {
                'count': self.page.paginator.count,
                'total_pages': self.page.paginator.num_pages,
                'current_page': self.page.number,
            },
            'results': data,
        })

5. PostgreSQL-Specific Optimisations

  • Use django.contrib.postgres.indexes.GinIndex for full-text search fields
  • Use F() expressions for atomic counter updates to avoid race conditions
  • Use bulk_create(update_conflicts=True) for upserts instead of looping saves
# Atomic increment — no race condition
Patient.objects.filter(id=patient_id).update(visit_count=F('visit_count') + 1)

Closing Thoughts

DRF rewards you when you know Django's ORM deeply. The framework gets out of your way once you stop fighting it with custom views for everything — lean into ViewSets, mixins, and the permission/throttle system, and your APIs will scale cleanly.