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.pyINSTALLED_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) in config/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

  • [ ] items is an installed app with its own api.py router
  • [ ] Mounted under /api/items without breaking core
  • [ ] 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