Django backend with uv
End state: a local Django project with one working JSON API endpoint, managed by uv (not pip/venv manually).
How to use this: do each step yourself. Do not skip ahead to copy a finished repo. After each step, glance at the "You should see" line before moving on. If something fails, fix that step — do not invent a parallel setup.
Commands assume you are in the project directory unless noted.
0. Prerequisites
- [ ] Install uv if you do not have it: https://docs.astral.sh/uv/getting-started/installation/
- [ ] Confirm it works:
uv --version
You should see a version number. Nothing else is required yet (no global Django, no manual python -m venv).
Why: uv creates the virtualenv, installs packages, and pins versions. You stop thinking about "is my venv activated?" — you prefix commands with uv run.
1. Create the folder and uv project
Pick a name for the outer folder (the thing you cd into). Example below uses myapi. Use your own name everywhere you see it.
mkdir myapi
cd myapi
uv init --no-package --python 3.13
- [ ] Run those three commands.
You should see files like:
pyproject.toml— project metadata and dependency list.python-version— which Python uv will useREADME.md— ignore for now- maybe a sample
hello.py/main.py— you can delete it later; it is not Django
Why --no-package: keeps a flat layout (manage.py at the root). Django expects that. You are not publishing a pip package.
Why pin Python: one less "works on my machine" variable. 3.12+ is fine if you prefer; keep whatever you pass to --python.
2. Add Django (and the API layer)
uv add django django-ninja
- [ ] Run that.
You should see:
pyproject.tomlnow listsdjangoanddjango-ninjaunderdependenciesuv.lockcreated (exact resolved versions).venv/created (do not commit this)
Peek at what landed:
uv run django-admin --version
You should see a Django version string.
Why uv add instead of pip: it updates pyproject.toml and the lockfile and the env in one move. Later, anyone (including future you) runs uv sync and gets the same stack.
Why Django Ninja: FastAPI-style routing and type hints on top of Django. You get JSON APIs and auto OpenAPI docs (/api/docs) without DRF's serializer ceremony.
3. Scaffold the Django project
Name the inner config package config (recommended). It holds settings/urls — not your business logic.
uv run django-admin startproject config .
The trailing . matters. Without it you get config/config/ nesting and a messier root.
- [ ] Run that from inside
myapi/.
You should see:
myapi/
├── manage.py
├── config/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── asgi.py
│ └── wsgi.py
├── pyproject.toml
├── uv.lock
└── .venv/
Mental model:
| Thing | Role |
|---|---|
Outer folder (myapi) |
Your git repo / uv project |
config/ |
Django project settings + root URL routes |
| Apps you create later | Features (users, orders, …) |
From now on, Django commands look like:
uv run python manage.py <command>
(uv run manage.py <command> often works too; the python manage.py form is unambiguous.)
4. Prove the server boots
Ninja does not need to be in INSTALLED_APPS to work. (Optional later: add "ninja" so the OpenAPI UI loads from Ninja's bundled JS instead of a CDN.)
Apply Django's built-in migrations (creates db.sqlite3 for local work):
uv run python manage.py migrate
Start the server:
uv run python manage.py runserver
- [ ] Open http://127.0.0.1:8000/ in a browser.
You should see Django's default success page (or a similar "it worked" page). Stop the server with Ctrl+C when done.
Why migrate now: Django's auth/sessions tables exist even before your apps do. Getting a green migrate early means your env and settings path are correct.
5. Create your first app
An app is a feature module. Name it for the domain, not "app1". Example: core as a thin starter API.
uv run python manage.py startapp core
- [ ] Run that.
You should see a core/ directory with models.py, views.py, apps.py, etc.
Register it in config/settings.py → INSTALLED_APPS:
"core",
(or "core.apps.CoreConfig" — both are fine at this stage)
- [ ] Save.
Why a separate app: config/ is wiring. Business code lives in apps. That split is what keeps Django projects navigable later.
6. One real JSON endpoint
You are not building the product yet. You are proving the request path: URL → Ninja → JSON.
Ninja uses a NinjaAPI (one per project, usually) and **Router**s (one per app/feature). You do not put API routes in Django urlpatterns per view — you mount api.urls once.
6a. App router
Create core/api.py (leave views.py alone for now — Ninja lives here):
from ninja import Router
router = Router()
@router.get("/health")
def health(request):
return {"status": "ok"}
- [ ] Save.
6b. Project API
Create config/api.py next to config/urls.py:
from ninja import NinjaAPI
from core.api import router as core_router
api = NinjaAPI()
api.add_router("", core_router)
- [ ] Save.
Why two files: config/api.py is the mount point. Each app exports a router. Later apps call api.add_router("/orders", orders_router) — same pattern, no new URL include style to learn.
6c. Wire into Django URLs
Open config/urls.py:
from django.contrib import admin
from django.urls import path
from .api import api
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", api.urls),
]
- [ ] Save.
6d. Hit it
uv run python manage.py runserver
In another terminal (from the same project directory):
curl http://127.0.0.1:8000/api/health
- [ ] You should see JSON like
{"status":"ok"}.
Also open http://127.0.0.1:8000/api/docs — Ninja's interactive OpenAPI UI. Confirm GET /api/health is listed.
That is the finish line for "starting point for a backend API."
7. Make the repo safe to commit
Create a .gitignore if you do not have one yet. At minimum:
.venv/
__pycache__/
*.py[cod]
db.sqlite3
.env
*.egg-info/
.DS_Store
- [ ] Add that file.
Commit when you want (optional for the runbook itself):
git init
git add .
git status # confirm .venv and db.sqlite3 are NOT staged
git commit -m "Initial Django API scaffold with uv"
Keep in git: pyproject.toml, uv.lock, Django source.
Keep out: .venv/, db.sqlite3, secrets.
Checkpoint — you are done when
- [ ]
uv syncwould recreate the env from lockfile (you already haveuv.lock) - [ ]
uv run python manage.py runserverstarts without import errors - [ ]
GET /api/healthreturns JSON - [ ]
/api/docsshows the endpoint - [ ] You can explain, in your own words: uv owns deps;
configowns settings +NinjaAPI;coreowns aRouter
If all five are true, stop. Do not keep scaffolding "while you're here." Open a ticket or note for the next real feature.
Everyday commands (cheat sheet)
| Intent | Command |
|---|---|
| Install deps on a fresh clone | uv sync |
| Add a package | uv add <package> |
| Add a dev-only package | uv add --dev <package> |
| Remove a package | uv remove <package> |
| Run any manage.py command | uv run python manage.py … |
| Django shell | uv run python manage.py shell |
| Make migrations after model changes | uv run python manage.py makemigrations |
| Apply migrations | uv run python manage.py migrate |
Next steps (only when you have a reason)
Do not treat these as part of day-one setup. Each step has its own runbook — open it when that need appears.
Suggested order when you will do several: environment → postgres and/or cors → auth → second app (second app can also happen anytime after this base runbook).
| # | When | Runbook |
|---|---|---|
| 1 | Sharing/deploying; stop hard-coding secrets | django-environment.md |
| 2 | SQLite is no longer enough | django-postgres.md (after env) |
| 3 | A separate frontend origin calls the API | django-cors.md (after env; before cookie auth) |
| 4 | You have a real “who can call this?” story | django-ninja-auth.md |
| 5 | A new domain that should not live in core |
django-second-app.md |
If you get stuck
| Symptom | Likely cause |
|---|---|
Failed to spawn: python / can't find project |
Not inside the folder with pyproject.toml |
No module named django |
Ran python manage.py without uv run, or never uv add/uv sync |
Nested config/config and no manage.py at root |
Forgot the trailing . on startproject |
ModuleNotFoundError: core / ninja |
App not in INSTALLED_APPS, or forgot uv add django-ninja / uv sync |
404 on /api/health |
api.urls not mounted in config/urls.py, router not add_router'd, or path typo (Ninja paths usually omit a trailing slash) |
Import error on from .api import api |
config/api.py missing or wrong package relative import |
Fix the matching row. Then re-run the smallest command that proves that layer (version → migrate → runserver → curl).