Deploy a Django Ninja API to Production
Take a Django Ninja API to production: choose WSGI or ASGI for sync and async endpoints, lock down the interactive docs, configure authentication, CORS, throttling, and error handling, and validate schemas in CI.
Problem statement
Django Ninja builds APIs with type hints and Pydantic schemas, generates OpenAPI documentation automatically, and supports async views. Moving a Ninja API from development to production raises the usual API questions plus a few specific ones: should it run under WSGI or ASGI, should the interactive docs at /api/docs be public, how are authentication and throttling configured, and how do validation errors and unexpected exceptions look to clients?
- Run under Gunicorn (WSGI) if endpoints are sync; under Uvicorn workers (ASGI) if you write
async defendpoints that await I/O. - Restrict or disable the docs in production:
NinjaAPI(docs_url=None)or protect them with staff-only auth. - Set authentication on the API or router (
auth=django_auth, API keys, or JWT) and check permissions per operation. - Enable throttling (
throttle=[AnonRateThrottle("60/m"), AuthRateThrottle("600/m")]) backed by a shared cache. - Add an exception handler that returns consistent JSON errors and logs unexpected ones.
- Export the OpenAPI schema in CI and diff it to catch breaking changes.
Step-by-step solution
1. Configure the API object
# api/api.py
from django.conf import settings
from ninja import NinjaAPI
from ninja.security import django_auth
from ninja.throttling import AnonRateThrottle, AuthRateThrottle
api = NinjaAPI(
title="Example API",
version="1.0.0",
auth=django_auth,
csrf=True,
docs_url="/docs" if settings.DEBUG else None,
throttle=[AnonRateThrottle("60/m"), AuthRateThrottle("600/m")],
)
api.add_router("/orders/", "orders.api.router")
django_auth uses the session cookie, and csrf=True enforces Django's CSRF check for cookie-authenticated unsafe requests. For token clients, use HttpBearer or API key classes per router instead.
2. Protect or remove the docs
The interactive docs list every endpoint and schema, which helps attackers map your API. Either disable them in production (as above) or keep them behind staff authentication:
from ninja.openapi.docs import Swagger
from django.contrib.admin.views.decorators import staff_member_required
api = NinjaAPI(docs=Swagger(), docs_decorator=staff_member_required)
3. Choose the server
# WSGI
gunicorn config.wsgi:application --workers 4 --bind unix:/run/gunicorn/api.sock
# ASGI, for async endpoints
gunicorn config.asgi:application -k uvicorn_worker.UvicornWorker --workers 4 --bind unix:/run/gunicorn/api.sock
Async endpoints in Ninja must not call the sync ORM directly; use await Model.objects.aget(...) or sync_to_async, or Django raises SynchronousOnlyOperation.
4. Throttling needs a shared cache
Ninja's throttles store counters in Django's cache. With LocMemCache, each Gunicorn worker counts separately and limits are multiplied by the worker count. Configure Redis as the default cache in production.
5. Consistent errors
from ninja.errors import ValidationError
import logging
log = logging.getLogger("api")
@api.exception_handler(ValidationError)
def validation_errors(request, exc):
return api.create_response(request, {"error": "invalid_request", "detail": exc.errors}, status=422)
@api.exception_handler(Exception)
def unexpected(request, exc):
log.exception("unhandled API error")
return api.create_response(request, {"error": "server_error"}, status=500)
Never return tracebacks or internal messages to clients; log them and report to Sentry.
6. Guard the contract in CI
python manage.py export_openapi_schema --api api.api.api --output openapi.json
git diff --exit-code openapi.json || echo "API schema changed: review for breaking changes"
Commit the schema and review diffs; tools such as oasdiff can flag breaking changes automatically.
Verification check: curl -i https://example.com/api/docs returns 404 (or a login redirect), unauthenticated calls to protected endpoints return 401, exceeding the anonymous rate returns 429, and an invalid payload returns a 422 JSON error without stack traces.
Explanation
Django Ninja runs inside Django: routing, middleware, authentication, and the ORM are all Django's, while Ninja adds type-driven parsing and validation through Pydantic and generates the OpenAPI schema from those types. Production concerns are therefore Django concerns (server choice, settings, caches) plus API-specific ones that Ninja exposes as options. Because the schema is generated from code, exporting and diffing it gives a cheap, automatic check that a refactor did not break clients.
Edge cases / notes
- CORS for browser clients on other origins uses
django-cors-headers, configured like any Django project. - Pagination with
@paginateavoids unbounded responses; set a maximum page size. - File uploads use
UploadedFileparameters; keepDATA_UPLOAD_MAX_MEMORY_SIZEand proxy body limits in mind. - Versioning. Mount
NinjaAPI(version="2.0.0", urls_namespace="api-2")under a new prefix for breaking changes.
Next steps
- Compare with Deploy Django REST Framework APIs in Production.
- Run through the Django API Production Checklist.
FAQ
Should Django Ninja run on WSGI or ASGI?
WSGI with Gunicorn for sync endpoints; ASGI with Uvicorn workers when async endpoints await I/O.
Should the Ninja docs be public in production?
Usually not. Disable them with docs_url=None or restrict them to staff.
Why does throttling not work across workers?
The cache is per process. Use Redis as the Django cache so counters are shared.
How do I avoid breaking API clients?
Export the OpenAPI schema in CI and review or automatically check diffs.