Django-ninja ? 🥷
Qu'est ce que c'est, Django-ninja ?
Django-ninja est une librairie python, qui, comme son nom peut le laisser deviner, s’intègre avec django. À quoi ça sert ? Comment on fait ? Et bien c’est ce que je vais raconter dans cet article.
Django-ninja est une alternative à django-rest-framework pour le développement d’API pour une application django. L’objectif est d’avoir une librarie simple d’utilisation et rapide, à la mise en place comme à l’exécution.
Pour l’écriture de cet article, j’ai développé un mini projet contenant deux modèles, un modèle Product
, contenant un nom, un prix et une Foreign key vers un modèle Category
, comportant un nom. Le code que j'ai écrit est dans un dépôt gitlab dont je mettrai le lien à la fin de l'article.
Comment ça marche ?
django-ninja s'inspire beaucoup de FastAPI, avec des endpoints déclarés par des fonctions.
La première étape est d'instancier une classe NinjaAPI
. En général, celle-ci sera déclarée dans un fichier api.py
situé au même niveau que le fichier urls.py
de votre projet. Dans l'extrait de code suivant, le paramètre urls_namespace
servira pour l'usage du raccourci reverse
de django.
from ninja import NinjaAPI
api = NinjaAPI(urls_namespace="api")
Ensuite, dans mon application, je vais instancier une classe Router
dans un fichier router.py
, de la façon suivante. Le paramètre tags
sera utilisé pour la génération de la documentation, pour regrouper ensemble les URL possédant le même tag.
from ninja import Router
router = Router(tags=["produits"])
Je vais ensuite retourner dans le fichier api
contenant mon instance de NinjaAPI
et pouvoir ajouter mon router à celle-ci. Dans mon projet, j'ai créé deux routers, un pour mon modèle Category
, l'autre pour mon modèle Product
.
from ninja import NinjaAPI
from demo.products import routers
api = NinjaAPI(urls_namespace="api")
api.add_router("/products/", routers.product.router)
api.add_router("/categories/", routers.category.router)
Et enfin, dans mon fichiers urls.py
, je vais pouvoir ajouter Ă la variable urlpatterns
les URL de mon API Ninja.
from django.urls import path
from .api import api
urlpatterns = [
path("api/", api.urls),
]
Ok, c'est super, j'ai pu ajouter les URL de mon api aux URL de django, mais maintenant, il va falloir définir des endpoints sinon ça nous fera une belle jambe. Ci-dessous vous trouverez un exemple d'un endpoint permettant de retrouver un Product
à partir de son id. Cette définition se fera dans le fichier router
créé plus tôt.
router = Router(tags=["produits"])
@router.get(
"/{id}/",
response={
HTTPStatus.OK: schemas.ProductOut,
HTTPStatus.NOT_FOUND: MessageSchema
},
url_name="product-details",
summary="Retrouver un produit par ID",
description="Point d'API permettant de retrouver un produit par son ID",
)
def retrieve_product(request, id: int):
try:
return HTTPStatus.OK, services.retrieve_product(id=id)
except exceptions.ErrorNotFound:
return HTTPStatus.NOT_FOUND, {
"message": f"Le produit avec l'id {id} n'a pas été trouvé"
}
La déclaration de l'endpoint se découpe en deux parties.
- L'utilisation du router comme décorateur, permettant en premier de déclarer le verbe HTTP, et en paramètre de celui-ci on va retrouver l'URL et ses éventuels paramètres et les types de réponses attendus (ici, une 200 avec le détail d'un produit, ou une 404 avec un message d'erreur). Le paramètre
url_name
définit le nom qu'il faudra utiliser pour le reverse. S'il n'est pas défini, il aura comme valeur par défaut le nom de la fonction. Les paramètressummary
etdescription
sont utilisés pour la génération de la documentation. - La déclaration de la fonction pour mon endpoint. En paramètre, j'aurai ma requête, ainsi que l'id que j'ai défini dans mon URL.
Dans mon code, j'ai fait le choix (sans doute discutable, en fonction des habitudes de code de chacun) de séparer ma logique métier de la gestion de mes endpoints. Ainsi, mes fonctions permettant de créer/lister/créer/modifier/supprimer des Product
en utilisant l'ORM sont placés dans un fichier services.py
. J'ai fait ce choix pour plusieurs raisons:
- Pour une question de lisibilité, j'aime bien séparer mon code et ranger les trucs, ainsi j'ai un fichier pour la gestion de mes endpoints, un pour mes services, et un autre pour mes schémas. Cependant, j'entends tout à fait que certains n'apprécient pas ce choix, vu que par nature, ça va impliquer de multiplier les fichiers.
- Pour pouvoir réutiliser mes fonctions de services ailleurs. Par exemple, ici, il y a de fortes chances que ma fonction service
retrieve_product
soit elle-même utilisée dans un autre endpoint permettant la modification d'unProduct
. - Si on pousse cette logique jusqu'au bout (ce que je n'ai pas fait dans mon projet), on peut avoir des services qu'on peut tester sans instancier de client http, et de l'autre des endpoints que l'on peut tester sans accès à une base de données, en mockant les fonctions services. Ça peut permettre d'accélérer les tests (au prix d'un peu plus d'écriture de code) et d'être plus précis sur ce que l'on teste.
Pour les types de réponses attendus, un Schema Ninja (basé sur Pydantic) est attendu pour décrire le format de données sortantes.
Comme Product
est un modèle django, je peux utiliser la classe ModelSchema
de django-ninja pour créer rapidement ce schéma, comme ci-dessous.
from ninja import ModelSchema
from . import models
class ProductOut(ModelSchema):
category_name: str
class Meta:
model = models.Product
fields = ["id", "name", "price", "category"]
Le format est assez similaire Ă ce qu'on peut retrouver dans les ModelForm
de django, avec l'usage d'une classe Meta
permettant de définir les champs voulus. Ici, j'ai également ajouté manuellement un champ category_name
qui fait référence à une property de mon modèle.
De la même façon que l'on peut définir un format de données en sortie dans le décorateur, on peut aussi définir un format de données en entrée avec un schéma. Par exemple, pour mon endpoint de création de Product
, la signature de ma fonction ressemblera à ça:
def create_product(request, payload: schemas.ProductIn):
...
Une fois les différents endpoints définis, un swagger sera généré pour l'API. Pour chaque endpoint seront décrits les paramètres, les codes de réponse possibles ainsi que le format de réponse pour chaque status_code.

Pourquoi je trouve ça intéressant?
J'ai pas mal pratiqué le DRF, et une des choses que je pourrais lui reprocher, c'est d'être monolithique, avec des classes qui font un peu tout. Par exemple, le ModelSerializer
de django s'occupe à la fois de valider le type de données en entrée, de faire des validations métier, de faire l'insertion en base de données, et de définir le format de sortie. Bref, il fait tout, tout seul, comme un champion. Et c'est très pratique en vrai. Mais j'ai aussi remarqué que dès lors qu'on a besoin faire beaucoup de validations custom, de gérer des cas spécifiques, ou de surcharger des méthodes, le serializer peut vite devenir conséquent.
J'ai l'impression qu'avec django-ninja, si on le couple à une bonne séparation des logiques et des responsabilités, la bonne compréhension du code peut être plus aisée, surtout pour des développeurs qui connaissent peu la librarie ; là ou je trouve que DRF, comme Django d'ailleurs, demande un certain temps d'apprentissage et d'adaptation pour bien comprendre le framework. Bien entendu, en contrepartie, on perd la "magie" de DRF qui fait plein de choses tout seul comme un grand, comme l'insertion des données en base.
Django-ninja est également relativement jeune, surtout par rapport à DRF et ses 14 ans d'existence (il grandit si vite...). DRF est un outil beaucoup plus complet et éprouvé que django-ninja. Typiquement, Ninja ne propose pas de gestion de token comme peut le faire DRF. La librarie propose des outils pour gérer l'authentification, mais c'est moins magique aussi. Cependant, de courageux développeurs ont déjà proposé des librairies permettant d'ajouter contenu et fonctionnalités à django-ninja, comme django-ninja-extra, qui introduit une notion de "api_controller", permettant une gestion des endpoints par classe et non par fonction ou encore django-ninja-jwt qui justement va permettre la gestion des tokens.
Sur ce, je vais m'arrêter de blablater ici, j'espère que la lecture de cet article vous a été agréable.
Ci-dessous, voici les deux liens promis :