Second app (after Django + uv)
Prerequisite: django-uv.md — especially the NinjaAPI + Router pattern (config/api.py + core/api.py).
End state: a second Django app with its own Ninja router mounted under a prefix (example: /api/items). core health check still works. You did not dump new domain code into core.
Suggested order: anytime after the base runbook. No dependency on Postgres/CORS/auth — but if those exist, new routes follow the same env/CORS/auth rules. If the new app needs a database model, have migrations ready (SQLite is fine; Postgres if you already switched).
How to use this: create one small read-only endpoint. Resist “while I’m here” models, admin, and permissions unless that is the actual goal of the day.
0. Why a second app
Put it in core |
Create a new app |
|---|---|
| Tiny shared helpers, health, truly global glue | A domain: items, billing, accounts, … |
| You would struggle to name the app | You can name it in one word |
If the feature has its own models and URLs, it wants its own app. That is the habit this runbook builds.
Example name below: items. Use your real domain word instead.
1. Create and register the app
uv run python manage.py startapp items
- [ ]
items/directory exists.
In config/settings.py → INSTALLED_APPS:
"items",
- [ ] Save.
2. Add a router for the app
Create items/api.py:
from ninja import Router
from ninja.schema import Schema
router = Router(tags=["items"])
class ItemOut(Schema):
id: int
name: str
@router.get("/", response=list[ItemOut])
def list_items(request):
# Hard-coded on purpose — proves routing before models.
return [
ItemOut(id=1, name="sample"),
]
- [ ] Save.
Why tags: groups operations in /api/docs.
Why a Schema: optional but teaches the Ninja response shape early; delete it later if you prefer a bare dict.
Why no model yet: routing and mounting are the lesson. Models come when you have fields to persist.
3. Mount it on the project API
Open config/api.py. It should look conceptually like:
from ninja import NinjaAPI
from core.api import router as core_router
from items.api import router as items_router
api = NinjaAPI()
api.add_router("", core_router)
api.add_router("/items", items_router)
- [ ] Save.
URL math:
path("api/", api.urls)inconfig/urls.py(already from the base runbook)- plus
add_router("/items", …) - plus
@router.get("/")
→ GET /api/items/ (Ninja may normalize trailing slashes; if one 404s, try the other once, then stick to what docs show).
Health remains /api/health from core.
4. Prove it
uv run python manage.py runserver
curl http://127.0.0.1:8000/api/health
curl http://127.0.0.1:8000/api/items/
- [ ] Health still ok.
- [ ] Items returns a JSON list with the sample row.
Open http://127.0.0.1:8000/api/docs — you should see an items group and the core/health operation.
5. Optional: protect only this app
If you already did django-ninja-auth.md:
api.add_router("/items", items_router, auth=api_key)
Or set Router(auth=api_key, tags=["items"]) inside items/api.py.
Leave /health on the unauthenticated core router.
- [ ] Skip unless auth already exists and this app should be private.
6. Optional: first model (only if you need persistence today)
In items/models.py:
from django.db import models
class Item(models.Model):
name = models.CharField(max_length=200)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
Then:
uv run python manage.py makemigrations items
uv run python manage.py migrate
Update list_items to query Item.objects and map to ItemOut. Register Item in items/admin.py only if you want the admin UI.
- [ ] Do this only when you are ready to own migrations — otherwise stop after the hard-coded list.
Checkpoint — you are done when
- [ ]
itemsis an installed app with its ownapi.pyrouter - [ ] Mounted under
/api/itemswithout breakingcore - [ ] You can explain: one
NinjaAPI, many routers, prefix decides the URL space
Stop. The next app repeats steps 1–4 with a new name. Do not merge everything back into core “for simplicity.”
If you get stuck
| Symptom | Likely cause |
|---|---|
ModuleNotFoundError: items |
Not in INSTALLED_APPS, or typo in import |
404 on /api/items |
Forgot add_router, wrong prefix, or trailing-slash mismatch |
| Docs show duplicate operationIds | Two routes with the same function name across routers — rename handlers |
| Circular imports | Don’t import api from apps; apps export routers, config/api.py imports them |
Related
- Env for new settings →
django-environment.md - Real DB →
django-postgres.md - Browser client →
django-cors.md - Lock the new router down →
django-ninja-auth.md