← Back to list

Django REST Framework Defaults That Fail In Production

DRF defaults are intentionally permissive and “it just works” — they help you build something quickly.

Django Wiki · 2026-03-23 13:01 · 2 claps · 7.9 min read
#django-rest-framework #drf #drf-serializer #django-framework #django
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Django REST Framework Defaults That Fail In Production

DRF defaults are intentionally permissive and “it just works” — they help you build something quickly.

But shipping with defaults usually means shipping implicit decisions you never reviewed.

Common symptoms you only notice later:

  • Endpoints that are technically “working” but returning huge payloads and slowing down under real traffic
  • Permission logic scattered across views because nothing was set globally
  • Serializers that accidentally expose fields you did not mean to expose
  • Error responses that look fine in the browsable API but inconsistent for real clients

Why this happens:

  • DRF tries to be flexible for different kinds of APIs (internal, external, admin-like, public)
  • Tutorials often show the smallest amount of configuration to keep the focus on building
  • Many safety knobs are optional because DRF cannot guess your business rules

A useful mental model:

  • Defaults are a draft. They are the “hello world” of API design.
  • Production is a contract. You are promising access rules, response shapes, performance, and stability.

If you are already deploying DRF, the best upgrade you can make is simple: open your REST_FRAMEWORK settings and force yourself to make every major decision explicitly.

Auth, Permissions, And Open Doors

The fastest way for a DRF API to fail in production is to accidentally leave a door open.

Here are the patterns that cause it.

Pattern 1: You never set global defaults

If you rely on view-by-view configuration, sooner or later someone adds a new endpoint and forgets permissions.

Start by setting safe global defaults.

# settings.py
REST_FRAMEWORK = {
    # Make auth and permissions explicit
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ),
    "DEFAULT_PERMISSION_CLASSES": (
        "rest_framework.permissions.IsAuthenticated",
    ),
}

Why this is better:

  • New endpoints start closed by default
  • Reviews become easier because exceptions are obvious
  • Your team stops repeating the same permission boilerplate

Pattern 2: AllowAny sneaks in

Sometimes it is explicit:

# views.py
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ReadOnlyModelViewSet

class PublicCatalogViewSet(ReadOnlyModelViewSet):
    permission_classes = [AllowAny]

That is fine — if it is truly public.

The problem is when AllowAny becomes the default in a base viewset, or gets copied into endpoints that are not meant to be public.

A safer approach is to create named permission policies so the intent is obvious.

# permissions.py
from rest_framework.permissions import BasePermission

class IsSupportAgent(BasePermission):
    """Example: user must be staff and belong to the support group."""
    def has_permission(self, request, view):
        user = request.user
        if not user or not user.is_authenticated:
            return False
        return user.is_staff and user.groups.filter(name="support").exists()

Then use it where it belongs:

# views.py
from rest_framework.viewsets import ModelViewSet
from .permissions import IsSupportAgent

class TicketViewSet(ModelViewSet):
    permission_classes = [IsSupportAgent]

Pattern 3: Session auth without CSRF awareness

SessionAuthentication is great for browser based APIs (and the browsable API), but it enforces CSRF checks.

That can surprise you if your “API” is used by:

  • Mobile apps
  • Third party clients
  • Single page apps using token based auth

Two practical ways to avoid confusion:

  • Use SessionAuthentication for internal admin-like endpoints
  • Use token or JWT based auth for external clients, and keep the auth story consistent across your API

The key is not “never use sessions” — it is “do not mix models accidentally”.

Pattern 4: Permissions do not match business roles

DRF permissions answer questions like:

  • Is the user authenticated?
  • Is the user admin?
  • Can the user access this object?

But your business rules are usually things like:

  • Can a customer view only their own orders?
  • Can a manager approve refunds?
  • Can a user modify resources inside their organization?

That is why custom permissions (plus queryset filtering) are normal in production.

A simple checklist for every endpoint:

  • What is the authentication method?
  • What is the default permission policy?
  • What is the object level rule?
  • Is the queryset filtered to match the same rule?

If any of these answers is “we did not decide” — a default decided for you.

Pagination, Throttling, And Performance Surprises

If you have ever seen a list endpoint go from “fine” to “down” overnight, it is usually one of these:

  • A mobile client started fetching more often than expected
  • A partner integrated your API with a loop
  • A scraper found your public endpoints
  • Your dataset grew and your list response grew with it

Defaults make this easy to miss because the API works in dev with small data.

Symptom: list endpoints return huge payloads

Without pagination, a simple GET /api/orders/ can return thousands of rows. That increases:

  • Database time
  • Serializer time
  • Response size
  • Client parsing time

Set a default pagination class globally, then override per endpoint when needed.

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 50,
}

Then keep list endpoints explicit about ordering (so page boundaries do not jump):

# views.py
from rest_framework import viewsets
from .models import Order
from .serializers import OrderSerializer

class OrderViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Order.objects.all().order_by("-created_at")
    serializer_class = OrderSerializer

If page number pagination does not fit your UX, switch to cursor pagination (stable ordering, better for feeds):

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.CursorPagination",
    "PAGE_SIZE": 50,
}

Symptom: public endpoints get abused

DRF throttling is not enabled unless you choose it. Without throttling, a single IP can:

  • Hammer login or password reset endpoints
  • Spam expensive search endpoints
  • Scrape your public data

Add throttling defaults, even if your first rates are conservative.

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "60/min",
        "user": "600/min",
    },
}

Then tighten per endpoint using scoped throttles when you have hot spots.

# views.py
from rest_framework.throttling import ScopedRateThrottle
from rest_framework.views import APIView
from rest_framework.response import Response

class LoginView(APIView):
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = "login"
    def post(self, request):
        # authenticate user here
        return Response({"detail": "ok"})
# settings.py
REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
        "rest_framework.throttling.ScopedRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "60/min",
        "user": "600/min",
        "login": "10/min",
    },
}

Symptom: defaults conflict with real world performance

Pagination and throttling are not just performance features. They are part of your API contract.

Ask yourself:

  • What is the largest response size you want to allow?
  • What is the expected request rate for real clients?
  • Which endpoints should have stricter limits (auth, search, exports)?

When you answer these explicitly, production traffic stops being a surprise.

Serialization, Validation, And Leaky Data

The fastest way to leak data in DRF is not a hack. It is a serializer that exposes more fields than you intended.

Pattern 1: fields = "__all__" becomes a data leak

It feels convenient while you build. Then you add a new model field later (for example is_staff_note or internal_status) and it ships to clients automatically.

Avoid implicit field exposure. Make fields explicit.

# serializers.py (unsafe)
from rest_framework import serializers
from .models import UserProfile

class UserProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserProfile
        fields = "__all__"
# serializers.py (safer)
from rest_framework import serializers
from .models import UserProfile

class UserProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserProfile
        fields = [
            "id",
            "display_name",
            "avatar_url",
            "bio",
            "created_at",
        ]
        read_only_fields = ["id", "created_at"]

Pattern 2: missing write_only and read_only for sensitive fields

Common examples:

  • Passwords should be write_only
  • Tokens should usually be write_only
  • Audit fields (created_at, updated_at) should be read_only
from rest_framework import serializers
from django.contrib.auth import get_user_model

User = get_user_model()

class SignupSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, min_length=12)

    class Meta:
        model = User
        fields = ["id", "email", "password"]
        read_only_fields = ["id"]

    def create(self, validated_data):
        return User.objects.create_user(
            email=validated_data["email"],
            password=validated_data["password"],
        )

Pattern 3: relying on model validation only

Model validation helps, but DRF serializers are where API rules belong because:

  • They can validate cross-field rules
  • They can validate request specific constraints
  • They can produce consistent errors for clients

Example: you want to prevent users from setting discount_percent above 50 unless they are staff.

from rest_framework import serializers
from .models import Coupon

class CouponSerializer(serializers.ModelSerializer):
    class Meta:
        model = Coupon
        fields = ["code", "discount_percent", "expires_at"]
    def validate_discount_percent(self, value):
        request = self.context.get("request")
        user = getattr(request, "user", None)
        if value > 50 and not (user and user.is_staff):
            raise serializers.ValidationError("Discount above 50% requires staff approval")
        return value

When you make these rules explicit, you stop shipping accidental behavior.

Error Handling, Logging, And API Contracts

DRF gives you a reasonable default exception handler, but production APIs usually need more consistency.

The problem is rarely the exception itself. It is the contract.

If some endpoints return:

  • {"detail": "Not found"}

And others return:

  • {"error": "Not found"}

And your custom code sometimes returns:

  • {"message": "Not found", "code": 404}

Then every client becomes harder to maintain.

Goal: one error shape your clients can trust

A simple approach is to wrap DRF errors into a consistent structure.

# api/exceptions.py
from rest_framework.views import exception_handler

def custom_exception_handler(exc, context):
    response = exception_handler(exc, context)
    if response is None:
        return None
    # Normalize DRF errors into a predictable envelope
    payload = {
        "error": {
            "type": exc.__class__.__name__,
            "detail": response.data,
        }
    }
    response.data = payload
    return response

Enable it globally:

# settings.py
REST_FRAMEWORK = {
    "EXCEPTION_HANDLER": "api.exceptions.custom_exception_handler",
}

Logging: the other half of error handling

Clients care about stable errors. You care about diagnosing the root cause.

At minimum, make sure unexpected errors are logged with request context.

# middleware.py
import logging

logger = logging.getLogger(__name__)
class LogUnhandledExceptionsMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    def __call__(self, request):
        try:
            return self.get_response(request)
        except Exception:
            logger.exception(
                "Unhandled exception",
                extra={
                    "path": request.path,
                    "method": request.method,
                    "user_id": getattr(getattr(request, "user", None), "id", None),
                },
            )
            raise

This middleware is intentionally simple. Your stack may already capture errors (Sentry, Datadog, etc). The point is: do not let 500s disappear.

API contracts include success responses too

Defaults let you return anything from anywhere. Production benefits from consistency:

  • consistent envelope (or consistent lack of envelope)
  • consistent field names
  • consistent status codes

Decide your contract once, and enforce it with serializers and shared response helpers.

Hardening DRF Settings For Real Traffic

Once you have felt the pain of “we shipped defaults”, the fix is not rewriting your app. It is making the implicit explicit.

Here is a production oriented baseline you can paste and adapt.

# settings.py
REST_FRAMEWORK = {
    # Auth and permissions
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.TokenAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],

    # Pagination
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 50,
    # Throttling
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
        "rest_framework.throttling.ScopedRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "60/min",
        "user": "600/min",
    },
    # Error handling
    "EXCEPTION_HANDLER": "api.exceptions.custom_exception_handler",
    # Renderers (typical for public APIs)
    "DEFAULT_RENDERER_CLASSES": [
        "rest_framework.renderers.JSONRenderer",
    ],
    # Optional: versioning (useful when your API changes over time)
    "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning",
}

A few practical notes to keep you out of trouble:

  • If you are a browser first app using session auth, do not switch blindly. Choose the auth story that matches your clients.
  • If you keep the browsable API in production, consider restricting it to internal staff, not anonymous users.
  • If you use JSON only renderers, make sure your clients send Accept: application/json (most already do).

Make endpoint intent visible in code

You will still need exceptions. The goal is to make them obvious.

Example: a health check endpoint that should be public.

from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response

@api_view(["GET"])
@permission_classes([AllowAny])
def healthcheck(request):
    return Response({"status": "ok"})

That is better than silently inheriting an insecure default.

7. A Production Ready DRF Defaults Checklist

Before you deploy (or the next time you touch settings.py), review these defaults like a checklist.

Auth and permissions

  • DEFAULT_AUTHENTICATION_CLASSES is set (and matches your clients)
  • DEFAULT_PERMISSION_CLASSES is set (and not accidentally AllowAny)
  • Admin or internal endpoints have stricter rules than public ones
  • Object level permissions and queryset filtering match the same business rule

Pagination and throttling

  • List endpoints paginate by default
  • Ordering is stable for paginated endpoints
  • Throttling is enabled for anon and user traffic
  • High risk endpoints (login, password reset, search) have tighter scoped rates

Serialization and validation

  • Serializers list explicit fields (avoid "__all__")
  • Sensitive fields are write_only and audit fields are read_only
  • Validation rules live in serializers (not only in models)

Errors and response formats

  • Error responses have a consistent shape across the API
  • Unexpected 500s are logged and visible to your team
  • You have decided what a “successful” response looks like

Settings that prevent surprises

  • Renderers are intentional (JSON only for many public APIs)
  • Versioning strategy exists if you expect breaking changes

One last question: if you open your current REST_FRAMEWORK settings right now, what is the one default you are still letting DRF decide for you?

Ahmad, a Django full-stack developer and creator of Django.wiki — a free learning platform for Django developers.


메타데이터
post_id
ff2a8770b7cd
slug
django-rest-framework-defaults-that-fail-in-production-ff2a8770b7cd
url
https://medium.com/@djangowiki/django-rest-framework-defaults-that-fail-in-production-ff2a8770b7cd
canonical_url
https://medium.com/@djangowiki/django-rest-framework-defaults-that-fail-in-production-ff2a8770b7cd
author_url
https://medium.com/@djangowiki
status
ok
fetched_at
2026-06-11 05:11:55