There's a false choice developers keep making: FastAPI for the nice typed API, or Django for the ORM, admin, migrations, and auth that you'd otherwise rebuild by hand. Django Ninja (https://django-ninja.dev) refuses the choice. It bolts a FastAPI-style routing layer — Pydantic schemas, async views, automatic OpenAPI docs — onto a real Django project. You get Swagger at /api/docs and python manage.py migrate in the same codebase.
This guide deploys a Django Ninja API to PandaStack with a managed PostgreSQL database.
Why Django Ninja over plain FastAPI
- Django ORM — mature migrations, a huge ecosystem,
select_related/prefetch_relatedthat actually work. - Django admin — a free CRUD backend for your data, no extra code.
- Django auth — sessions, permissions, password hashing, all solved.
- Ninja on top — typed request/response schemas, async endpoints, and OpenAPI docs generated from your type hints.
If you're already a FastAPI shop with SQLModel and no need for the admin, plain FastAPI is fine. If you keep wishing FastAPI had Django's ORM and admin, this is your framework.
Step 1: A minimal Django Ninja app
api/views.py:
from ninja import NinjaAPI, Schema
from django.contrib.auth.models import User
api = NinjaAPI()
class UserOut(Schema):
id: int
username: str
email: str
@api.get("/health")
def health(request):
return {"status": "ok"}
@api.get("/users", response=list[UserOut])
def list_users(request):
return list(User.objects.all())
@api.get("/users/{user_id}", response=UserOut)
def get_user(request, user_id: int):
return User.objects.get(id=user_id)Wire it in urls.py:
from django.urls import path
from api.views import api
urlpatterns = [
path("api/", api.urls), # docs live at /api/docs automatically
]Step 2: Production settings from environment variables
In settings.py, read everything from the environment — never hardcode secrets:
import os
import dj_database_url
SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "False") == "True"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",")
DATABASES = {
"default": dj_database_url.config(default=os.environ["DATABASE_URL"])
}Step 3: requirements.txt
django>=5.0
django-ninja>=1.3
gunicorn>=21.0
uvicorn[standard]>=0.29
psycopg[binary]>=3.1
dj-database-url>=2.1
whitenoise>=6.6Note uvicorn — Django Ninja's async endpoints need an ASGI server to actually run concurrently.
Step 4: Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput
EXPOSE 8000
# Gunicorn with uvicorn workers = ASGI (async) in production
CMD ["gunicorn", "myproject.asgi:application", \
"-k", "uvicorn.workers.UvicornWorker", \
"--bind", "0.0.0.0:8000", "--workers", "2"]Replace myproject with the package that contains asgi.py. Using wsgi:application instead would silently make your async endpoints run synchronously — a classic performance mystery.
Step 5: Create the managed PostgreSQL database
- 1Dashboard (https://dashboard.pandastack.io) → Databases → New Database → PostgreSQL.
- 2When you attach it to your app,
DATABASE_URLis injected automatically — you don't paste the connection string anywhere.
Step 6: Deploy
- 1Push to GitHub.
- 2New App → Container App, connect the repo (PandaStack detects the Dockerfile), container port 8000.
- 3Attach the PostgreSQL database.
- 4Set environment variables:
| Variable | Value |
|---|---|
SECRET_KEY | python -c "import secrets; print(secrets.token_urlsafe(50))" |
DEBUG | False |
ALLOWED_HOSTS | your-api.pandastack.io |
- 1Deploy. Every push to your default branch redeploys.
CLI path:
npm install -g @pandastack/cli
panda login
panda deployStep 7: Run migrations
The reliable pattern is a release step so migrations run once before the new version serves traffic. An entrypoint script:
#!/bin/sh
python manage.py migrate --noinput
exec gunicorn myproject.asgi:application \
-k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 --workers 2Or run one-off via the CLI:
panda exec --command "python manage.py migrate"Step 8: Verify
Your API is live at https://your-api.pandastack.io:
- Interactive docs:
https://your-api.pandastack.io/api/docs - Health:
curl https://your-api.pandastack.io/api/health→{"status":"ok"}
Create a superuser to reach the Django admin:
panda exec --command "python manage.py createsuperuser"Then https://your-api.pandastack.io/admin/ gives you a full CRUD backend for free.
Performance notes
- Match
--workersroughly to available CPU. For async-heavy workloads, fewer workers with more concurrency often beats many workers. - Add Redis (managed on PandaStack) for caching and to back Django's cache framework.
- Serve static files with WhiteNoise (already in requirements) — no separate static host needed.
Wrap-up
Django Ninja is the "why not both" answer: typed, async, documented endpoints with Django's ORM, admin, and auth underneath. On PandaStack it's a container app + managed Postgres + a migrate step, deployed on Git push. Docs: https://docs.pandastack.io. Start free at https://dashboard.pandastack.io.