skip to content

Django Ninja

Django Ninja declares API operations with type hints and Pydantic schemas, validates input and generates OpenAPI docs automatically. Interviewers ask when you would pick it over DRF.

on this pageshow

explore

questions

19

In Django Ninja, how do you protect a partner operation with an APIKeyHeader authenticator, and what does request.auth hold afterwards?

level: juniorimportance: must knowfreq 50%

answer

  1. subclass, then one method
  2. which header name, by default
  3. truthy return value is kept
  4. falsy everywhere means 401

basics

~10 s

Subclass ninja.security.APIKeyHeader, set param_name to the header, and implement authenticate(request, key) returning the partner or None; pass an instance as auth=. Ninja stores the truthy return value in request.auth, or answers 401.

solid answer

~40 s

I subclass `ninja.security.APIKeyHeader`, set `param_name = "X-API-Key"` - the default is a header literally called `key` - and implement `authenticate(self, request, key)`, which looks the key up and returns the matching `Partner` or `None`. An instance goes on `auth=` of the operation, a `Router` or the `NinjaAPI`. Before the view runs, Ninja calls the authenticator; any **truthy** return value is stored in `request.auth`, so the view reads `request.auth` to know which partner is calling. A falsy result makes Ninja try the next authenticator in the list, and if none succeeds it answers `401` with `{"detail": "Unauthorized"}` without calling the view. `APIKeyQuery` and `APIKeyCookie` work the same way from the query string and a cookie, and `HttpBearer` passes the token from `Authorization: Bearer ...`.

code

python · 38 lines
python
import hashlib

from django.db import models
from ninja import NinjaAPI, Schema
from ninja.security import APIKeyHeader


class Partner(models.Model):
    name = models.CharField(max_length=100)
    key_digest = models.CharField(max_length=64, unique=True)
    is_active = models.BooleanField(default=True)

    def __str__(self):
        return f"partner:{self.pk}"


class PartnerKey(APIKeyHeader):
    param_name = "X-API-Key"

    def authenticate(self, request, key):
        if not key:
            return None
        digest = hashlib.sha256(key.encode()).hexdigest()
        return Partner.objects.filter(key_digest=digest, is_active=True).first()


api = NinjaAPI()


class OrderIn(Schema):
    sku: str
    quantity: int


@api.post("/partner/orders", auth=PartnerKey())
def submit_order(request, payload: OrderIn):
    partner = request.auth  # the Partner returned by authenticate()
    return {"partner": partner.name, "sku": payload.sku}

go deeper

for a junior

Recall the pattern: subclass APIKeyHeader, set param_name, implement authenticate(), pass an instance to auth=, read request.auth in the view.

for a middle

Explain the chain: list order, truthy wins, falsy moves on, raising stops everything, and 401 when all decline.

for a senior

Return an identity object rather than True, keep keys out of query strings, and choose header, bearer or cookie authenticators knowing which ones trigger CSRF checks.

for a principal

Decide how partner identity flows from the authenticator into authorisation and rate limiting, so request.auth is a stable, documented contract across the API.

## What auth= expects In **Django Ninja**, an operation is protected by passing an **authenticator** to `auth=`. An authenticator is any callable that takes the Django `request` and returns something: a **truthy** value means the caller is authenticated, and Ninja stores that value in **`request.auth`**; a falsy value (`None`, `False`, an empty string) means "not me". The built-in classes in `ninja.security` are ready-made callables that extract a credential from the request and hand it to a method you write. | class | where it reads the credential | method you implement | |---|---|---| | `APIKeyHeader` | `request.headers[param_name]` | `authenticate(request, key)` | | `APIKeyQuery` | `request.GET[param_name]` | `authenticate(request, key)` | | `APIKeyCookie` | `request.COOKIES[param_name]`, with a CSRF check | `authenticate(request, key)` | | `HttpBearer` | `Authorization: Bearer <token>` | `authenticate(request, token)` | | `HttpBasicAuth` | `Authorization: Basic <base64>` | `authenticate(request, username, password)` | | `django_auth` | the Django session (`request.user`) | nothing - it is a ready instance | For the three API-key classes, **`param_name` defaults to `"key"`**. An `APIKeyHeader` subclass that forgets to set it reads a header named `key`, so every partner sending `X-API-Key` gets a 401. ## Building a partner authenticator The steps for a partner endpoint are: 1. Subclass `APIKeyHeader` and set `param_name = "X-API-Key"`. 2. Implement `authenticate(self, request, key)`. `key` is `None` when the header is absent. 3. Look the key up and return the **object that identifies the caller** - a `Partner` instance - or `None`. 4. Pass an **instance** to `auth=`: `@api.post("/partner/orders", auth=PartnerKey())`. 5. In the view, read `request.auth` to get the partner. How the key is generated, hashed at rest and rotated is key-management design, not Ninja API; the authenticator only has to turn a header value into a caller or `None`. ## What happens on each request Before parameter validation and before the view, Ninja runs the operation's authenticators **in list order**: - the first one that returns a truthy value wins; its value goes into `request.auth` and the rest are skipped; - a falsy value moves on to the next authenticator; - an authenticator that **raises** - for example `raise HttpError(403, "Partner suspended")` - ends the chain immediately, and the exception's handler produces the response; - if every authenticator returns something falsy, Ninja raises `ninja.errors.AuthenticationError`, rendered as **401** `{"detail": "Unauthorized"}`. So `auth=[PartnerKey(), django_auth]` accepts either a partner key or a logged-in Django user, and the view must cope with `request.auth` being either a `Partner` or a `User`. ## Returning something useful `request.auth` is exactly what `authenticate()` returned. Returning `True` passes the check but leaves the view no way to know who called, and it breaks anything keyed on the caller, such as per-caller throttling that uses `str(request.auth)`. Returning the `Partner` gives the view the identity and lets authorisation checks read its fields, for example which partner owns the order being updated. ## Header, query or bearer - **`APIKeyHeader`** suits server-to-server partners: the key stays out of URLs. - **`APIKeyQuery`** puts the key in the URL, where it tends to end up in access logs and browser history; keep it for cases where headers cannot be set. - **`HttpBearer`** is the choice when the credential is a bearer token; it strips the `Bearer` scheme (case-insensitively) and returns `None` for a missing header or a different scheme. - **`APIKeyCookie`** and `django_auth` read cookies, which browsers attach automatically, so Ninja runs a CSRF check for them on unsafe methods. ## Testing an authenticator `ninja.testing.TestClient` calls operations directly and makes the behaviour easy to pin down. Three cases cover most of it: - a request without the header returns 401 and never reaches the view; - a request with an unknown or inactive key returns 401; - a request with a valid key returns 200, and the view sees the right partner in `request.auth`. The client accepts `headers={"X-API-Key": "..."}` on each call. A fourth test that sends the key under the wrong header name catches a missing `param_name` before a partner does, and a test that returns an inactive partner proves the lookup filters on status rather than only on the key. ## Mistakes interviewers probe - Passing the class instead of an instance: `auth=PartnerKey` makes Ninja call the class with the request. - Forgetting `param_name` and debugging 401s for a header that is never read. - Returning a queryset from `authenticate()`: an unevaluated queryset is truthy even when empty, so a wrong key passes. - Expecting `request.user` to be the partner: `request.user` comes from Django's session middleware, while Ninja puts the authenticator's result in `request.auth`.

  • How do you reject a known but suspended partner with 403 instead of 401 in Django Ninja?
    Raise from `authenticate()`: `raise HttpError(403, "Partner suspended")` from `ninja.errors`, or `AuthorizationError()`, which defaults to 403. An exception ends the authenticator chain and goes to the API's exception handlers, while returning `None` would only produce the generic 401 once every authenticator declined.
  • What does HttpBearer do with a header like Authorization: Token abc?
    It returns `None` without calling `authenticate()`, because the scheme is compared case-insensitively against `bearer`. With `DEBUG` on it also logs the unexpected value. The request then falls through to the next authenticator or ends in a 401.

saying these in an interview costs you the question

  • APIKeyHeader reads the X-API-Key header unless told otherwise.
  • request.user is set to whatever authenticate() returns.
  • Returning True from authenticate() is as good as returning the partner.
  • When authentication fails, the view runs with request.auth set to None.
  • All authenticators in the list must pass for the request to proceed.
open as a page

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%

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.

open as a page

In Django Ninja, when a posted JSON body fails the operation's Schema, where does the 422 come from and what does it contain?

level: juniorimportance: must knowfreq 55%

basics

~10 s

Django Ninja validates every declared parameter before calling the view, collects the failures into ninja.errors.ValidationError, and NinjaAPI's default handler for that exception returns 422 with a detail list of type, loc and msg entries.

open as a page

In Django Ninja, with auth= set on the NinjaAPI, a Router and an operation, which applies, and how is one operation made public?

level: middleimportance: must knowfreq 45%

basics

~10 s

The most specific auth= replaces the others: operation, then the add_router() mount, then Router(auth=), then a parent router, then NinjaAPI(auth=). Settings are never merged. auth=None on an operation or router makes it public.

open as a page

In Django Ninja, how does an operation decide whether a parameter comes from the path, the query string or the request body?

level: middleimportance: must knowfreq 62%

basics

~20 s

Django Ninja applies rules in order: an explicit Query/Path/Body/Form/File/Header/Cookie marker wins; a name in the path is a path parameter; a list, set, tuple or Schema is body; any other scalar is a query parameter.

open as a page

In Django Ninja, when would you declare a plain Schema instead of a ModelSchema, and what must a ModelSchema's Meta class contain?

level: middleimportance: must knowfreq 50%

basics

~20 s

A Schema is a hand-written Pydantic model; a ModelSchema derives its fields from a Django model through Meta, which needs model plus exactly one of fields or exclude. Use Schema when the API contract differs from the table, typically for input.

open as a page

Why does a Django Ninja async def search operation that returns Product.objects.filter(...) with response=list[ProductOut] fail, and how do you write it correctly?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Ninja awaits the async view, then serialises the result synchronously; iterating the lazy queryset there hits the database from the event loop, and Django raises SynchronousOnlyOperation. Evaluate it in the view with async iteration and select_related any relations the schema reads.

open as a page

In Django Ninja, where do the generated OpenAPI schema and the /docs page come from, and how do you configure or hide them?

level: juniorimportance: should knowfreq 40%

basics

~10 s

NinjaAPI builds an OpenAPI 3.1 document from each operation's type hints, schemas, response= and auth=, serves it at openapi.json and renders it at /docs with Swagger UI. docs_url=None hides the page; openapi_url=None hides both.

open as a page

In Django Ninja, how does the @paginate decorator turn a list operation into a paginated one, and what limits should you set?

level: middleimportance: should knowfreq 40%

basics

~20 s

@paginate, placed under the operation decorator on a list response, adds paging query parameters, slices the queryset the view returns and wraps it as {items, count}. LimitOffsetPagination is the default, and its limit is uncapped unless you configure a maximum.

open as a page

In Django Ninja, how do AnonRateThrottle, AuthRateThrottle and UserRateThrottle differ in which requests they count and how they key them?

level: middleimportance: should knowfreq 30%

basics

~20 s

AnonRateThrottle counts only requests without request.auth, keyed by client IP; AuthRateThrottle keys by a hash of str(request.auth); UserRateThrottle keys by the Django request.user's primary key. The last two fall back to IP for anonymous callers.

open as a page

In Django Ninja, how do you register a custom exception handler, and how does NinjaAPI decide which handler handles a raised exception?

level: middleimportance: should knowfreq 40%

basics

~10 s

Decorate a function with @api.exception_handler(SomeError) on the NinjaAPI instance; it takes request and exc and returns a response. NinjaAPI walks the exception's MRO and uses the first class with a registered handler.

open as a page

In a Django Ninja Schema, how does a resolve_<field> method compute an output field, and what constraints and costs come with it?

level: middleimportance: should knowfreq 30%

basics

~20 s

Django Ninja collects resolve_<field> methods when the Schema class is created and calls one instead of reading the attribute; in 1.7.1 it must be a @staticmethod taking the source object, optionally context, and it runs once per object.

open as a page

In Django Ninja, how does a dict of status codes passed to response= work, and how is the return value's schema chosen?

level: middleimportance: should knowfreq 45%

basics

~20 s

response={201: BookingOut, 409: Message} maps each status to a schema; the view returns Status(409, body) to choose one, and Ninja validates and filters the body through that schema. Returning an undeclared status raises ConfigError, a 500.

open as a page

Why does a Django Ninja POST guarded by auth=[django_auth, PartnerBearer()] return 403 to a partner sending a valid bearer token, and how do you fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

django_auth is cookie-based, so on unsafe methods it runs Django's CSRF check first and raises HttpError(403) when it fails; the raise ends the chain before PartnerBearer runs. Put PartnerBearer first or give partners a separate router.

open as a page

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?

level: seniorimportance: should knowfreq 30%

basics

~10 s

In 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.

open as a page

A Django Ninja booking endpoint returns 500 in production, and a plain-text traceback locally, when its return value breaks the response schema - why not 422, and how do you fix it?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Response validation raises pydantic.ValidationError, not ninja.errors.ValidationError, so only the generic Exception handler catches it: a traceback under DEBUG, Django's 500 otherwise. It is a server bug; fix the schema or the returned data.

open as a page

For a new Django API, when would you choose Django Ninja over Django REST Framework, and what would keep you on DRF?

level: principalimportance: should knowfreq 45%

basics

~20 s

Django Ninja suits typed, schema-first APIs: Pydantic validation, OpenAPI generated from type hints, native async operations. DRF suits teams already on it, and APIs leaning on viewsets, object-level permission classes, the browsable API and its third-party ecosystem. Both can share one project.

open as a page

In Django Ninja, why does /products/{product_id} answer 422 for a non-integer id while /products/{int:product_id} answers 404?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

Ninja turns {product_id} into Django's default str converter, which matches 'abc', so Pydantic validation of product_id: int fails with 422. With {int:product_id}, Django's int converter does not match, so the resolver returns 404 and Ninja never runs.

open as a page

A Django Ninja partner endpoint uses AuthRateThrottle('600/m'), yet in production partners share one limit or exceed it freely - what do you check?

level: seniorimportance: nice to knowfreq 20%

basics

~10 s

Check that str(request.auth) is unique per partner, since AuthRateThrottle hashes it; that CACHES is a shared backend rather than per-process LocMemCache; and, for IP fallbacks, NINJA_NUM_PROXIES. Throttles also share keys across operations.

open as a page