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?

Django Ninja in productionClient requestJSON over HTTPSNinjaAPI routerauth, throttlePydantic schemavalidate in and outApp serverWSGI or ASGINinja validates every request and response with schemas; the server choice depends on async use.
  1. Run under Gunicorn (WSGI) if endpoints are sync; under Uvicorn workers (ASGI) if you write async def endpoints that await I/O.
  2. Restrict or disable the docs in production: NinjaAPI(docs_url=None) or protect them with staff-only auth.
  3. Set authentication on the API or router (auth=django_auth, API keys, or JWT) and check permissions per operation.
  4. Enable throttling (throttle=[AnonRateThrottle("60/m"), AuthRateThrottle("600/m")]) backed by a shared cache.
  5. Add an exception handler that returns consistent JSON errors and logs unexpected ones.
  6. 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)
which server for Ninja?What do the endpoints look like?all sync, ORM-heavyGunicorn (WSGI)sync workers or gthreadasync def calling APIsUvicorn workers (ASGI)gunicorn -k uvicornmixedASGIsync views run in threadsAsync only pays off when endpoints await slow I/Odjango-deployment.com
Async endpoints must use the async ORM or sync_to_async for database calls.

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.

Ninja API launch checksDocs disabled or staff-onlydocs_url=None in prodAuth on API and routerssession + CSRF, or tokensThrottles backed by Redisnot per-worker memoryJSON error handlersno tracebacks to clientsOpenAPI schema diffed in CIcatch breaking changesSchemas make Ninja APIs safe to evolve when the contract is checkeddjango-deployment.com
Pair it with the general API production checklist for headers and limits.

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 @paginate avoids unbounded responses; set a maximum page size.
  • File uploads use UploadedFile parameters; keep DATA_UPLOAD_MAX_MEMORY_SIZE and 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

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.

Keep going