After splitting a Django Ninja inventory API into routers and adding a second NinjaAPI, startup raises ConfigError and reverse() hits the wrong API; what causes these failures?
answer
- generating URLs freezes the templates
- two APIs, one default namespace
- the same router mounted twice
- mount everything in one module
basics
~10 sIn Ninja 1.7, reading api.urls freezes routers, so a late add_router() or operation raises ConfigError; two default NinjaAPI instances share the api-1.0.0 namespace; and mounting one router twice needs a distinct url_name_prefix per mount.
solid answer
~40 sNinja 1.7 binds routers when `api.urls` is first read, which Django does while loading `urls.py`, and then freezes them: any later `add_router()` or new operation raises `ConfigError`, usually because a module registering operations is imported lazily or routers are mounted outside the API module. Two `NinjaAPI()` instances both default to the namespace `api-` plus version, `api-1.0.0`; Ninja allows it, Django warns `urls.W005`, and `reverse()` can reach only one — so give each API its own `urls_namespace` or `version`. Mounting one router twice raises `ConfigError` unless each mount has a distinct `url_name_prefix`, which then prefixes the URL names. The discipline: one module creates each API, sets its namespace and mounts every router, preferably by dotted path.
code
python · 21 lines# inventory/api.py: the only place routers are mounted
from ninja import NinjaAPI
public_api = NinjaAPI(title="Inventory", urls_namespace="inventory")
public_api.add_router("/products/", "products.api.router")
public_api.add_router("/v1/stock/", "stock.api.router", url_name_prefix="v1")
public_api.add_router("/v2/stock/", "stock.api.router", url_name_prefix="v2")
internal_api = NinjaAPI(title="Back office", urls_namespace="backoffice")
internal_api.add_router("/audit/", "audit.api.router")
# inventory/urls.py
from django.urls import path
from inventory.api import internal_api, public_api
urlpatterns = [
path("api/", public_api.urls),
path("internal-api/", internal_api.urls),
]go deeper
Know that all routers must be mounted before urls.py includes api.urls, and that each NinjaAPI has a URL namespace.
Explain freezing on api.urls, the api-1.0.0 default namespace, and why a repeated mount needs url_name_prefix to keep URL names unique.
Trace each startup or reverse() failure to its cause and restructure registration so one module mounts everything, with a test that reverses one URL per router.
Decide how many NinjaAPI surfaces the project exposes and the namespace and versioning scheme each one follows.
## The scenario An inventory API has grown from one `NinjaAPI` with a handful of operations to a router per app — products, stock, warehouses — plus a second, internal API for back-office tools. After the refactor, three symptoms appear: the server fails at startup with a `ConfigError`, the internal API's URLs reverse to the public API's paths, and a router meant for both `/v1/` and `/v2/` refuses to mount twice. Each has a specific cause in how Django Ninja 1.7 registers routes. ## Symptom 1: ConfigError at startup Ninja 1.7 treats routers as **templates** that are bound to the API when URLs are generated. Reading `api.urls` — which Django does when it loads `urls.py` — builds the URL patterns and **freezes** every mounted router. After that: - `api.add_router(...)` raises `ConfigError`: "Cannot add routers after URLs have been generated. Add all routers before accessing api.urls". - Declaring a new operation on a frozen router, or calling its `add_router()`, raises `ConfigError`: "Cannot modify router after URLs have been generated. Routers become frozen when api.urls is accessed." Typical causes: - A module that registers operations is imported **after** `urls.py` ran — for example imported lazily from inside a view, a signal handler or a test. - Routers are mounted in code that runs later than the URLconf, instead of in the API module itself. The fix is structural: the API module (`inventory/api.py`) mounts every router at import time — by object or by dotted path, `api.add_router("/stock/", "stock.api.router")`, which also guarantees the router module is imported — and nothing mounts routers anywhere else. ## Symptom 2: reverse() resolves to the wrong API Each `NinjaAPI` puts its URLs in a namespace that defaults to `"api-" + version`. Two APIs created as `NinjaAPI()` both use **`api-1.0.0`**. Ninja 1.7 does not stop you; Django's system check warns **`urls.W005`** ("URL namespace 'api-1.0.0' isn't unique. You may not be able to reverse all URLs in this namespace"), and `reverse("api-1.0.0:list_products")` can only reach one of them. Ninja's documentation is explicit: different `NinjaAPI` instances need different `version`s or different `urls_namespace`s. ```python public_api = NinjaAPI(title="Inventory", urls_namespace="inventory") internal_api = NinjaAPI(title="Back office", urls_namespace="backoffice") ``` ## Symptom 3: one router, two prefixes Mounting the same `Router` object twice on one API is allowed in 1.7, but because each mount would generate the same URL names, Ninja requires a distinct **`url_name_prefix`** per mount; without it, the second `add_router()` raises `ConfigError` ("Router is already mounted to this API ..."). With prefixes, URL names become `<prefix>_<function name>`: ```python api.add_router("/v1/stock/", stock_router, url_name_prefix="v1") api.add_router("/v2/stock/", stock_router, url_name_prefix="v2") # reverse("inventory:v2_list_stock") ``` Each mount gets its own cloned operations, so mount-level `auth`, `throttle` and `tags` can differ between the two. ## Why Ninja freezes routers Freezing is a consequence of the 1.7 design, not an arbitrary rule. When `api.urls` is generated, each mount **clones** the router's operations and applies what it inherits — the API's and the mount's `auth`, `throttle`, `tags` and decorators — before turning them into Django URL patterns. Django keeps those patterns for the life of the process. An operation added afterwards would exist on the router but in no URL pattern, and a router mounted afterwards would never be routed; raising `ConfigError` turns that silent gap into an immediate, visible failure. ## Smaller checks worth running during the refactor | Check | What it catches | |---|---| | `manage.py check` output | `urls.W005` duplicate namespaces | | Warnings at import | A path placeholder with no matching parameter ("in the view path, but were not found in the view signature") | | `ConfigError` "Duplicated name" | Two flattened query schemas defining the same field name | | `ConfigError` about `x=Schema` | A schema used as a default instead of an annotation | | `/api/docs` after each change | Operations missing because their router was never mounted | ## A registration discipline that prevents all three 1. **One module per API** creates the `NinjaAPI`, sets `urls_namespace`, and mounts every router, by dotted path where import order is fragile. 2. **Routers declare only operations and their own `tags`**; they never mount themselves. 3. **Every extra API gets its own namespace**, and every repeated mount its own `url_name_prefix`. 4. **A test reverses one URL per router** in each namespace, so a missing mount or a namespace clash fails the suite instead of production.
- Why does mounting by dotted path help with import-order problems?`add_router("/stock/", "stock.api.router")` imports the router module at the moment of mounting, inside the API module, before `urls.py` reads `api.urls`. Every operation in that module is then registered before the routers freeze, and nobody has to remember an import elsewhere.
- Can one Router be shared by two different NinjaAPI instances in Ninja 1.7?Yes. Routers are templates: each API binds its own cloned operations when it generates URLs, so the same router can appear in a public and an internal API with different auth or throttling. Within a single API, repeated mounts still need distinct `url_name_prefix` values.
saying these in an interview costs you the question
- Adding routers from views or signal handlers after startup
- Leaving two NinjaAPI instances on the default version and namespace
- Mounting one router twice on an API without url_name_prefix
- Ignoring urls.W005 in manage.py check output
- Assuming routers are namespaces of their own for reverse()