Django REST Framework Defaults That Fail In Production
DRF defaults are intentionally permissive and “it just works” — they help you build something quickly.
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
SessionAuthenticationfor 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_CLASSESis set (and matches your clients)DEFAULT_PERMISSION_CLASSESis set (and not accidentallyAllowAny)- 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_onlyand audit fields areread_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