Django REST API Production Checklist

Review a Django REST API before production: authentication, permissions, throttling, pagination, validation, versioning, errors, CORS, docs, and observability.

Problem statement

An API has a different risk profile from a website. Clients are programs: they retry aggressively, send unexpected input, cache responses, and break when a field changes type. Mistakes are also less visible, because there is no page rendering incorrectly for someone to notice; instead, an integration quietly fails at 3 a.m. or a public endpoint leaks data no browser ever displayed.

This checklist reviews a Django REST Framework API before it is exposed to real clients. It assumes DRF, but the principles apply to any Django API, including Django Ninja or plain views returning JSON.

An API request in productionClienttoken or sessionThrottlingrate limitsViewvalidation, queriesResponsepaginated, cachedCheck each stage: authentication, limits, correctness, and response size.

Quick answer

  1. Defaults: authentication required, JSON renderer only, pagination on, throttling on.
  2. Permissions: every endpoint's access rules are explicit and tested, including object-level checks.
  3. Input: validated by serializers with size limits; no mass assignment of sensitive fields.
  4. Output: only intended fields; no accidental exposure of internal or personal data.
  5. Errors: consistent JSON error responses for 4xx and 5xx, including outside DRF views.
  6. Compatibility: versioning strategy, additive changes, and a deprecation process.
  7. Operations: structured request logs, metrics per route, rate limits at the proxy, and documentation that matches reality.

Checklist

1. Safe framework defaults

REST_FRAMEWORK = {
    "DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"],
    "DEFAULT_PARSER_CLASSES": ["rest_framework.parsers.JSONParser"],
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 50,
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {"anon": "30/minute", "user": "600/minute"},
    "NUM_PROXIES": 1,
}

What to check:

  • the browsable API renderer is disabled in production
  • default permissions are restrictive, so a view missing explicit permissions is closed
  • pagination applies to every list endpoint, with a bounded max_page_size
  • throttle rates are backed by a shared cache (Redis), not local memory

2. Authentication

What to check:

  • the authentication scheme fits the clients: session authentication for same-site browser apps, tokens or JWT for machines and mobile apps
  • tokens are revocable; DRF's default TokenAuthentication tokens do not expire on their own, so add rotation or expiry if long-lived credentials are a concern
  • JWT settings (if used) have short access token lifetimes, a refresh mechanism, and a way to invalidate compromised tokens
  • credentials travel only over HTTPS, and plain HTTP is rejected at the proxy rather than redirected
  • WSGIPassAuthorization On (Apache) or equivalent, so the Authorization header reaches Django
  • login and token endpoints are throttled more strictly than normal endpoints
match authentication to the clientSuitsWatch forSession + CSRFyour own browser frontendCSRF on every unsafe methodToken / API keyservers and scriptsrotation and revocationJWTmobile, many services!short expiry, no easy revokeOAuth 2third-party appsscopes and consentSessions are still the safest choice for a same-site browser appdjango-deployment.com
Tokens stored in browser localStorage are readable by any script on the page.

3. Permissions and object-level access

What to check:

  • every view sets permission_classes intentionally, even when the default is restrictive
  • object-level permissions are enforced for detail endpoints; get_queryset() filters by ownership rather than relying on unguessable IDs:
class OrderViewSet(viewsets.ModelViewSet):
    serializer_class = OrderSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Order.objects.filter(customer=self.request.user.customer)
  • write endpoints check the same ownership as read endpoints
  • tests cover "another user's object returns 404 or 403" for each resource
  • staff-only actions are not reachable by regular tokens

4. Input validation

What to check:

  • serializers define explicit fields; avoid fields = "__all__" for writable serializers, which can expose or accept fields you did not intend
  • read-only fields (id, created_at, owner, status transitions) are marked read_only
  • numeric and string limits (max_length, min_value, max_value) are set
  • nested writes are controlled, not accidental
  • file uploads validate type and size, and are stored outside the web root
  • request size limits configured (DATA_UPLOAD_MAX_MEMORY_SIZE, DATA_UPLOAD_MAX_NUMBER_FIELDS, proxy client_max_body_size)
  • query parameters used for filtering and ordering are restricted to a known list, so clients cannot order by arbitrary columns or filter on sensitive fields

5. Output control

What to check:

  • serializers expose only intended fields; no password hashes, internal notes, tokens, or other users' personal data
  • error messages do not leak implementation details (SQL, file paths, stack traces)
  • list endpoints do not include heavy nested structures by default; provide expansion parameters instead
  • consistent field naming and types across endpoints, since clients depend on them
decide what leaves the serverfields = "__all__"✕new model fields ship automatically✕internal flags and notes leak✕writable fields you never meantExplicit fields✓each field a deliberate choice✓read_only_fields for server data✓separate write serializers if neededA migration should never change what an API exposes by accidentdjango-deployment.com
Mass assignment through writable fields is a classic API vulnerability.

Review the actual payloads:

curl -s -H "Authorization: Token $TOKEN" https://api.example.com/api/orders/ | python3 -m json.tool | head -40

6. Error responses

What to check:

  • DRF returns JSON for handled exceptions; unmatched URLs and unhandled errors also return JSON through handler404 and handler500
  • validation errors have a consistent shape clients can parse
  • 429 responses include Retry-After
  • 500 responses are generic in content but logged in full internally
  • proxy-generated errors (413, 502, 504) are documented for clients, ideally with a JSON body
what a production error body containsHTTP/1.1400Bad RequestContent-Type: application/json{"detail": "Invalid input.","errors": {"email": ["Enter a valid email address."]},"request_id": "b7c1e9…"}1231The status code carries the meaning; never 200 with an error.2Field-level messages a client can show.3Matches the log line, so support can find it.No traceback, SQL, or internal paths, even on a 500django-deployment.com
Use one shape for every error so clients need one parser.

7. Performance

What to check:

  • list endpoints use select_related and prefetch_related to avoid N+1 queries
  • serializer method fields do not issue per-object queries
  • expensive aggregates use annotations
  • pagination limits are enforced server-side
  • caching for read-heavy public endpoints where appropriate, with correct Vary headers
  • query counts asserted in tests for key endpoints

8. Rate limiting and abuse protection

What to check:

  • DRF throttles for per-user and per-endpoint limits
  • proxy-level rate limiting for cheap protection against floods
  • NUM_PROXIES or equivalent configured so throttling identifies clients correctly behind proxies
  • limits documented for API consumers, with headers or documentation explaining them
  • expensive endpoints (search, exports, report generation) have stricter limits or run asynchronously

9. CORS and browser clients

What to check:

  • CORS_ALLOWED_ORIGINS lists exact origins; no wildcard with credentials
  • CorsMiddleware is placed above middleware that can return responses early
  • preflight requests succeed for the methods and headers clients use
  • CSRF_TRUSTED_ORIGINS set for session-authenticated browser clients

10. Versioning and change management

What to check:

  • a versioning approach is decided (URL path such as /api/v1/, headers, or none for internal APIs)
  • additive changes only within a version; breaking changes create a new version
  • deprecation process: announce, add Deprecation or Sunset headers, monitor usage of old endpoints, remove after a defined period
  • clients can discover the current version and changelog

11. Documentation

What to check:

  • an OpenAPI schema is generated (for example with drf-spectacular) and matches the implementation
  • schema and interactive docs are appropriately exposed: public for public APIs, authenticated for internal ones
  • examples show authentication, pagination, filtering, and error responses
  • documentation is regenerated in CI so it cannot drift

12. Observability

What to check:

  • structured request logs with method, route pattern, status, duration, and client identity (not raw paths with IDs)
  • metrics per route: request rate, error rate, latency percentiles, and throttle rejections
  • errors reported to an error tracker with request context
  • alerts on 5xx rate, sustained 429 rate, and latency regressions
  • a health endpoint for load balancers that does not require authentication or throttling
from one request to an alertRequest IDgenerated or accepted, returned ina headerStructured logsendpoint, status, duration, userMetricsrate, errors, latency per endpointAlertson error rate and p95 latencyEach layer is built from the one below itdjango-deployment.com
Without the request ID, a user report cannot be matched to a log line.

13. Deployment settings

What to check:

  • DEBUG = False, correct ALLOWED_HOSTS, HTTPS enforced, HSTS enabled
  • SECURE_PROXY_SSL_HEADER set when behind a TLS-terminating proxy
  • Gunicorn worker and timeout settings suited to API traffic (short timeouts, threads for I/O-bound work)
  • background processing for anything slow, returning 202 Accepted with a status URL

Verify

# Unauthenticated access is rejected
curl -s -o /dev/null -w "%{http_code}\n" https://api.example.com/api/orders/

# Another user's object is not accessible
curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Token $OTHER_TOKEN" https://api.example.com/api/orders/1042/

# Pagination and throttling
curl -s -H "Authorization: Token $TOKEN" "https://api.example.com/api/orders/?page_size=10000" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d['results']), d['count'])"
for i in $(seq 1 40); do curl -s -o /dev/null -w "%{http_code} " https://api.example.com/api/public/status/; done; echo

Verification check:

  • unauthenticated and cross-tenant requests are rejected
  • page_size cannot exceed the configured maximum
  • throttling returns 429 with Retry-After
  • unknown routes return JSON 404
  • documented schema matches actual responses

Explanation

APIs expose data and behavior directly, with clients that do not forgive inconsistency. The safest defaults are restrictive (authentication required, JSON only, paginated, throttled), with each endpoint opening exactly what it needs. Explicit input and output control prevents both injection-style problems and accidental data exposure, while versioning and observability make the API maintainable once real clients depend on it.

Edge cases / notes

Public read-only APIs

Even without authentication, apply pagination, throttling, caching, and careful field selection; public endpoints attract scrapers.

Webhooks you receive

Incoming webhooks need signature verification, replay protection, and idempotent processing, and should be excluded from browser-oriented protections such as CSRF while remaining authenticated by signature.

Mobile clients

Old app versions stay in use for a long time. Plan for long support windows on API versions, and use feature flags to change behavior without forcing upgrades.

Common mistakes

  • Leaving the browsable API and AllowAny defaults in production.
  • Relying on unguessable IDs instead of queryset filtering for authorization.
  • fields = "__all__" on writable serializers.
  • No pagination limit, allowing a single request to fetch an entire table.

Next steps

FAQ

What are safe DRF defaults for production?

JSON renderer only, explicit authentication classes, IsAuthenticated as the default permission, pagination with a maximum page size, and throttling backed by a shared cache.

How should a Django API handle authorization for detail endpoints?

Filter the queryset by ownership in get_queryset() and enforce object-level permissions, so another user's object returns 404 or 403 rather than data.

Does a Django API need CSRF protection?

Session-authenticated browser requests do; token-authenticated requests from non-browser clients do not. DRF's SessionAuthentication enforces CSRF for unsafe methods.

How do I version a Django REST API?

Choose one scheme, commonly a URL prefix such as /api/v1/, keep changes additive within a version, and introduce breaking changes in a new version with a documented deprecation period.

Keep going