Framework
eadwinCode/django-ninja-extra avatar
eadwinCode/django-ninja-extra

Django Ninja Extra adds controllers and dependency injection to Django Ninja

Django Ninja Extra - Class-Based Utility and more for Django Ninja(Fast Django REST framework)

593 stars59 forksPythonMIT

At a glance

What is it?
Django Ninja Extra layers class-based controllers, a Django Rest Framework style permission system and Injector powered dependency injection on top of Django Ninja. It is a useful pattern for larger APIs, and it also adds a second opinionated framework to your stack, with a version 0.22.2 deprecation that changed how services are declared.
Who is it for?
Adopt Django Ninja Extra if your API has grown past a handful of function based endpoints and you want controllers, per route permissions and injected services without leaving Django Ninja. Do not adopt it for a small API, where plain Django Ninja plus Django's own tools are less to learn, and read the 0.22.2 deprecation notice before upgrading if you use ModelController, because the service attribute changed from a class to an instance.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 15 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A second layer on top of Django Ninja

Django Ninja Extra is described as a powerful extension for Django Ninja that enhances your Django REST API development experience, introducing class-based views and advanced features while maintaining the high performance and simplicity of Django Ninja.

The case for it is scale of the API, not of the application. A dozen endpoints as plain functions in one module is a perfectly good shape for Django Ninja, and adding a controller layer on top of that is overhead. A hundred endpoints with shared dependencies, mixed permission rules and a service layer wants structure, and that structure is what this package provides.

The core features are inherited from Django Ninja and are listed as such: Pydantic based validation, async and await support, type hints, Django integration, automatic OpenAPI documentation with Swagger and ReDoc, and API throttling.

The extra features are the reason the package exists. Class-based controllers let you organize related endpoints in controller classes, inherit common functionality and share dependencies across endpoints. The permission system is described as similar to Django Rest Framework, with controller-level permissions, route-level permission overrides and custom permission classes. And there is dependency injection with built-in support for Injector, compatibility with django_injector and automatic dependency resolution, plus a service layer for injectable services that hold business logic.

The primary language is Python, the licence is MIT, the default branch is master, the last push was on 2026-10-08, and the release history shows 0.31.5 in June 2026, 0.31.6 in July and 0.31.7 with Ninja v1.6.3 in August.

Installing it and mounting the API

Installation is a pip install and one entry in INSTALLED_APPS:

bash
pip install django-ninja-extra
python
INSTALLED_APPS = [
    ...,
    'ninja_extra',
]

Then you create an api.py with a NinjaExtraAPI instance rather than the plain Ninja one, and register your controllers on it:

python
from ninja_extra import NinjaExtraAPI, api_controller, http_get

api = NinjaExtraAPI()

# Function-based endpoint example
@api.get("/hello", tags=['Basic'])
def hello(request, name: str = "World"):
    return {"message": f"Hello, {name}!"}

# Class-based controller example
@api_controller('/math', tags=['Math'])
class MathController:
    @http_get('/add')
    def add(self, a: int, b: int):
        """Add two numbers"""
        return {"result": a + b}

    @http_get('/multiply')
    def multiply(self, a: int, b: int):
        """Multiply two numbers"""
        return {"result": a * b}

# Register your controllers
api.register_controllers(MathController)

The important detail is that function-based endpoints still work. The controller is an addition, not a replacement, so a project can adopt it incrementally, which is a sensible way to introduce a new pattern into a codebase that already has endpoints.

The URL configuration is standard Django, mounting the API under a prefix:

python
from django.urls import path
from .api import api

urlpatterns = [
    path("api/", api.urls),  # This will mount your API at /api/
]

And the interactive documentation lands at /api/docs, served by the same object.

Permissions at controller and route level

The permission model is the feature that most directly borrows from Django Rest Framework, and the example shows the two levels clearly:

python
from ninja_extra import api_controller, http_get
from ninja_extra.permissions import IsAuthenticated, PermissionBase

# Custom permission
class IsAdmin(PermissionBase):
    def has_permission(self, context):
        return context.request.user.is_staff

@api_controller('/admin', tags=['Admin'], permissions=[IsAuthenticated, IsAdmin])
class AdminController:
    @http_get('/stats')
    def get_stats(self):
        return {"status": "admin only data"}
    
    @http_get('/public', permissions=[])  # Override to make public
    def public_stats(self):
        return {"status": "public data"}

The controller declares a default permission set, IsAuthenticated and IsAdmin together, and a route can override it with an empty list to make a single endpoint public. That is the same model as Django Rest Framework's permission_classes on a view, and it is the reason someone would reach for this package rather than a handful of functions.

A custom permission extends PermissionBase and implements has_permission, which receives a context rather than the raw request, so the context is where the request is reached from. The context object is a detail worth checking against your own conventions, because a permission class written for Django Rest Framework will not drop in unchanged.

The other part of the feature set that touches the framework is the API throttling listed under the inherited Django Ninja features, which is the rate limiting that ships with Django Ninja rather than something this package adds.

Services injected into controllers

The dependency injection story is built on the Injector package, and the pattern is that a controller takes its service as a constructor argument:

python
from injector import inject
from ninja_extra import api_controller, http_get


# Service class
class UserService:
    def get_user_details(self, user_id: int):
        return {"user_id": user_id, "status": "active"}


# Controller with dependency injection
@api_controller('/users', tags=['Users'])
class UserController:
    def __init__(self, user_service: UserService):
        self.user_service = user_service

    @http_get('/{user_id}')
    def get_user(self, user_id: int):
        return self.user_service.get_user_details(user_id)

The type annotation on the constructor argument is what the injector uses to resolve the dependency, so the service is constructed for the controller rather than created inside it. That is the payoff of the service layer, a controller does not know how to build the thing it uses, so a test can substitute it.

There is a deprecation notice at the end of the README that matters here, and it is about version 0.22.2. The service attribute in ModelController has been changed from a class object to an instance object, so when creating a custom ModelService for a ModelController you have to specify it as service_type. The reason given is that services are now injected as dependencies during controller instantiation, and service instantiation is delegated to the injector package, which means any additional dependencies your ModelService needs must be properly registered in the dependency injection container.

That is the sharp edge of this package. A change to how a service attribute is declared, in a package whose version numbers are in the 0.3x range, is a reminder to read the change notes before a Django upgrade rather than after.

The requirements in the README and the requirements in the metadata differ

The README lists the requirements as Python 3.6 or greater, Django 2.1 or greater, Pydantic 1.6 or greater and Django-Ninja 0.16.1 or greater. The package metadata in pyproject.toml declares something noticeably stricter: Django 2.2 or greater, django-ninja 1.7.0 or greater, injector 0.19.0 or greater, plus asgiref and contextlib2. The build backend is flit_core with a version range of 3.12.0 to 4, so the module name is ninja_extra while the distribution name is django-ninja-extra.

If you are installing today, the metadata is what pip enforces, and the README floors are historical. The gap matters for two reasons. A project on Django Ninja 0.x will not satisfy the declared requirement, so the extension is not a drop-in for an older Ninja codebase. And the Python classifiers stop at 3.8 and go up to 3.13, while the README says 3.6, so the supported range in the metadata is the one to plan against.

The Django classifier list is also a useful compatibility signal, naming Django 3.1, 3.2, 4.1, 4.2, 5.0 and 6.0, which tells you the versions this package has been exercised against. The AsyncIO environment classifier reflects the async support that comes from Django Ninja itself.

The repository layout is a conventional Python package with a .flake8 config, an .isort.cfg, a .pre-commit-config.yaml, a .coveragerc, a pytest.ini, separate requirements files for docs and tests, a Makefile and a mkdocs.yml with a docs/ directory. The Makefile targets show the workflow: install uses flit install --symlink, lint runs ruff check and mypy, fmt runs ruff format and ruff check --fix, test runs pytest, test-cov adds coverage, and doc-serve and doc-deploy run mkdocs.

When plain Django Ninja is the better answer

The honest case against this package is that it adds a framework on top of a framework, and a small API does not need one.

Django Ninja already gives you Pydantic validation, type hints, async views, OpenAPI generation and throttling with none of the ceremony of Django Rest Framework. For a project with, say, twenty endpoints and a couple of services, the controller layer adds a decorator on the class, a registration call, and a conceptual step, in exchange for grouping that a module and a docstring would have given you for free.

The dependency injection is the other place where the trade has to be made honestly. Injector gives you constructor injection and a container, and it removes the need to write factory functions and pass them around. But it is a container you now configure, with a lifecycle you now own, and a place where a circular dependency becomes a configuration error rather than a simple import error. Django's own lazy settings and module level imports are a different, less magical answer to the same problem.

The features that are hardest to do without this package are the ones that look like Django Rest Framework: per route permission overrides on a group of endpoints, and services injected into a class. If your API is mostly flat and public, you do not need it. If it has an admin area, a public area and a service layer, the structure pays for itself.

There is also the ecosystem cost. You now have two libraries to keep in step with Django and with Ninja, and the release history shows that this package pins a Ninja version in its own releases, 0.31.7 with Ninja v1.6.3, which means an upgrade of one is an upgrade of both. That is manageable, but it is a dependency to track rather than a library you add and forget.

Editorial conclusion

Adopt Django Ninja Extra if your API has grown past a handful of function based endpoints and you want controllers, per route permissions and injected services without leaving Django Ninja. Do not adopt it for a small API, where plain Django Ninja plus Django's own tools are less to learn, and read the 0.22.2 deprecation notice before upgrading if you use ModelController, because the service attribute changed from a class to an instance. Before you start, check the version requirements, because the README lists much older floors than the package metadata actually declares.

Frequently asked questions

How do I install Django Ninja Extra?

Run `pip install django-ninja-extra` and add 'ninja_extra' to INSTALLED_APPS in your Django settings. The package is built with flit, and the distribution name is django-ninja-extra while the importable module is ninja_extra.

What does Django Ninja Extra add to Django Ninja?

Class-based controllers that group related endpoints and can be registered with api.register_controllers, a permission system similar to Django Rest Framework with controller-level rules and route-level overrides, and dependency injection built on the Injector package with a service layer for business logic.

How do I override permissions for a single route in Django Ninja Extra?

Pass a permissions list to the route decorator, for example `@http_get('/public', permissions=[])`, which overrides the permissions declared on the controller. A custom permission extends PermissionBase and implements has_permission, which receives a context object.

What changed in version 0.22.2 of Django Ninja Extra?

The service attribute in ModelController changed from a class object to an instance object, so a custom ModelService now has to be specified as service_type. Services are injected as dependencies during controller instantiation and instantiation is delegated to the injector package, so any extra dependencies of your ModelService must be registered in the container.

Official sources

  1. eadwinCode/django-ninja-extra on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/eadwincode-django-ninja-extra.svg)](https://hysenlabs.com/projects/eadwincode-django-ninja-extra)

Was this useful?