skip to content

In Django Ninja, how do NinjaAPI, Router and add_router() fit together, and how is the API mounted in urls.py?

level: juniorimportance: must knowfreq 58%

answer

  1. one API object, many groups
  2. each app keeps its own group
  3. a prefix per group
  4. one include-style entry in urlpatterns

basics

~10 s

NinjaAPI 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 s

A 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
python
# 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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