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 use
  • README.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.toml now lists django and django-ninja under dependencies
  • uv.lock created (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.pyINSTALLED_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 sync would recreate the env from lockfile (you already have uv.lock)
  • [ ] uv run python manage.py runserver starts without import errors
  • [ ] GET /api/health returns JSON
  • [ ] /api/docs shows the endpoint
  • [ ] You can explain, in your own words: uv owns deps; config owns settings + NinjaAPI; core owns a Router

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).