In Django Ninja, how do NinjaAPI, Router and add_router() fit together, and how is the API mounted in urls.py?
answer
- one API object, many groups
- each app keeps its own group
- a prefix per group
- one include-style entry in urlpatterns
basics
~10 sNinjaAPI is the API; each app defines a Router whose decorated functions are operations; api.add_router(prefix, router) mounts each router; and urls.py includes everything once with path('api/', api.urls), which also serves /docs and /openapi.json.
solid answer
~40 sA project creates one `NinjaAPI`, usually in an `api.py` beside `urls.py`. Each app defines `router = Router()` in its own `api.py` and declares operations with `@router.get`, `@router.post` and friends, where the first argument is the Django request. The project API mounts them with `api.add_router('/products/', products_router)` — or a dotted path like `'stock.api.router'` — and routers can nest with their own `add_router()`. `urls.py` then needs one entry, `path('api/', api.urls)`; `api.urls` returns the URL patterns, the app name `ninja` and the namespace, `api-1.0.0` by default, so `reverse('api-1.0.0:get_product', ...)` works with the function name as the URL name. The same include serves `/api/docs` and `/api/openapi.json`.
code
python · 18 lines# products/api.py
from django.shortcuts import get_object_or_404
from ninja import Router
from .models import Product
router = Router(tags=["products"])
@router.get("/")
def list_products(request):
return list(Product.objects.values("id", "sku", "name"))
@router.get("/{product_id}")
def get_product(request, product_id: int):
product = get_object_or_404(Product, id=product_id)
return {"id": product.id, "sku": product.sku, "name": product.name}go deeper
Recall the three pieces: one NinjaAPI, a Router per app with decorated operations, add_router() to mount it, and path('api/', api.urls) in urls.py.
Explain what api.urls returns, how URL names and the api-1.0.0 namespace are derived, and how prefixes from Django, the router and the operation combine.
Show how you keep a multi-app API maintainable: routers per app with tags, dotted-path mounting, distinct namespaces for extra APIs, and all mounting done before api.urls is read.
Decide how the API surface is partitioned, one NinjaAPI or several, and what versioning and namespace conventions the team follows.
## The three pieces A Django Ninja API is built from three objects, all imported from `ninja`: - **`NinjaAPI`** — the API itself. It owns the settings that apply to everything: `title`, `version` (default `"1.0.0"`), `docs_url` (default `/docs`), `openapi_url` (default `/openapi.json`), the URL namespace, the renderer and parser, exception handlers, and default auth and throttling. A project usually has one, in a module such as `inventory/api.py` next to `urls.py`. - **`Router`** — a group of operations, typically one per Django app, in that app's `api.py`. It has the same decorators as the API and can carry its own `tags`, `auth` and `throttle`. - **Operations** — plain functions decorated with `@api.get`, `@router.post`, `@router.put`, `@router.patch`, `@router.delete`, or `@router.api_operation(["GET", "POST"], ...)` for several methods. The first parameter is always the Django `request`; the rest are typed parameters Ninja validates. ## Wiring an inventory API together For an inventory service with a `products` app and a `stock` app: 1. **Each app declares a router** — `router = Router()` in `products/api.py`, with operations such as `@router.get("/")` and `@router.get("/{product_id}")`. 2. **The project API mounts them** — `api.add_router("/products/", products_router)`. `add_router()` also accepts a dotted import path, `api.add_router("/stock/", "stock.api.router")`, which avoids importing the module by hand. 3. **`urls.py` includes the API once** — `path("api/", api.urls)`. `api.urls` is a property returning Django's include-style tuple: the generated URL patterns, the application name `"ninja"`, and the instance namespace. 4. **Routers can nest** — `products_router.add_router("/{product_id}/images/", images_router)` builds deeper paths; an operation in the nested router reads the prefix's path parameter with `Path(...)`. The resulting URLs join the Django prefix, the router prefix and the operation path: `/api/products/`, `/api/products/<product_id>`, `/api/stock/...`, plus `/api/docs` for the interactive documentation and `/api/openapi.json` for the schema of every mounted operation. ## What an operation receives and returns - **Arguments** — the Django `HttpRequest` first, then one argument per declared parameter, already converted to its annotated type; a value that fails conversion never reaches the function, because Ninja answers `422` first. - **Return value** — a `dict`, a `list` or a schema instance is rendered to JSON by the API's renderer, with status `200` unless the operation declares otherwise; a Django `HttpResponse` returned directly is passed through untouched, which is the escape hatch for files or redirects. - **Operation options** — the decorators also take `summary`, `description`, `tags`, `operation_id`, `deprecated`, `url_name` and `include_in_schema`, which shape the generated docs and URL names without changing behaviour. ## Names, namespaces and reverse() Every generated URL pattern gets a **name**, by default the operation function's `__name__`, inside the API's **namespace**, by default `"api-" + version` — `"api-1.0.0"` for a default `NinjaAPI()`. So: ```python from django.urls import reverse reverse("api-1.0.0:get_product", kwargs={"product_id": 7}) # "/api/products/7" ``` - `url_name="product_detail"` on the decorator overrides the name. - `urls_namespace="inventory"` on `NinjaAPI` overrides the namespace; do this whenever a project has more than one `NinjaAPI`, or change `version`, because two APIs sharing a namespace cannot both be reversed. - The docs and schema views are named `openapi-view` and `openapi-json` in the same namespace. ## How it compares with Django's own URLconf | Concern | Plain Django views | Django Ninja | |---|---|---| | Where a route is declared | A `path()` entry in `urlpatterns` | The operation decorator, e.g. `@router.get("/{product_id}")` | | Grouping | `include()` of an app URLconf | `Router` mounted with `add_router()` | | Method dispatch | Inside the view, or a class-based view's method handlers | Usually one function per method (or `api_operation` for several); undeclared methods get `405` | | Path syntax | `<int:product_id>` | `{product_id}` or `{int:product_id}`, translated to Django's syntax | | Input parsing | By hand from `request.GET` and `request.body` | From the function's type hints | Ninja still produces ordinary Django URL patterns: middleware, the URL resolver and `reverse()` work exactly as for any other view. The API is one `include`-style entry in the project URLconf. ## Rules that save debugging time - **Mount every router before `api.urls` is read.** In Ninja 1.7, reading `api.urls` freezes the routers; a later `add_router()` or a new operation raises `ConfigError`. - **Keep one `NinjaAPI` per public surface**, and give additional ones a distinct `urls_namespace` or `version`. - **Let routers own `tags`** (`Router(tags=["stock"])` or `add_router(..., tags=[...])`) so the generated docs group operations by app.
- What does api.urls actually return?A three-item tuple Django's `path()` accepts in place of `include()`: the generated URL patterns (the operations' routes, plus the OpenAPI JSON, the docs view and an API root), the application name `"ninja"`, and the instance namespace, which defaults to `"api-" + version`. Reading it also freezes the mounted routers in Ninja 1.7.
- How do two operations share one path, such as GET and PUT on /products/{product_id}?Declare two functions with the same path string, one with `@router.get` and one with `@router.put`, or one function with `@router.api_operation(["GET", "PUT"], ...)`. Ninja groups operations by path into one Django view that dispatches on the HTTP method and returns `405 Method not allowed` for a method no operation declares.
saying these in an interview costs you the question
- Adding a separate urls.py entry for every Ninja router or operation
- Expecting Ninja to autodiscover Router objects in each app
- Reversing Ninja URLs without the api-1.0.0 style namespace
- Creating a second NinjaAPI with the same version and default namespace
- Mounting routers after urls.py has already read api.urls