Django Ninja quickstart
Créer des apis rapidement dans Django: une promesse que tient Django Ninja. Directement inspirée de FastAPI et utilisant les mêmes concepts, cette librairie permet de créer des endpoints flexibles et maintenables. Nous allons explorer l'écriture d'une api avec Django Ninja. Le code contenu dans cet article peut être complété par un repository starter template django-spaninja et par la documentation officielle de Django Ninja
Créer un endpoint
Nous allons créer un endpoint GET simple à partir du modèle User standard de Django. Dans un fichier api.py dans une app Django nommée account:
from typing import Tuple
from ninja import Router
from django.http import HttpRequest
from django.contrib.auth import logout
router = Router(tags=["account"])
@router.get(
"/logout",
response={200: None, 403: None},
)
def auth_logout(request: HttpRequest) -> Tuple[int, None]:
if request.user.is_anonymous is True:
return 403, None
logout(request) # returns a 200
return 200, None
Status codes http
Ici le endpoint ne retourne pas de données. Les status codes http sont déclarés dans le decorator de la fonction, indiquant au frontend le résultat du traitement. Le endpoint renverra un code 403 si l'utilisateur est anonyme, sinon un 200 après avoir délogué le user
Définir le schéma de réponse
Une des grandes forces de Django Ninja réside dans sa capacité à valider les données entrantes et sortantes grâce aux schémas Pydantic. Nous allons créer un endpoint GET simple qui retourne de l'information sur un user. Définissons d'abord le schéma de réponse dans un fichier schemas.py:
from django.contrib.auth import get_user_model
from ninja.schema import ModelSchema
class UserSchema(ModelSchema):
class Meta:
model = get_user_model()
fields = ["username", "first_name", "last_name"]
Ce schéma déclare les champs username, first_name et last_name du modèle User. Nous allons l'utiliser pour définir le data type de la réponse http du endpoint:
from django.contrib.auth import get_user_model
from ninja import Router
from .schemas import UserSchema
router = Router()
@router.get(
"/users/{user_id}",
response=UserSchema,
status={200: UserSchema, 404: None},
)
def get_user(request: HttpRequest, user_id: int):
try:
user = get_user_model().objects.get(id=user_id)
except get_user_model().DoesNotExist:
return 404, None
return user, None
Paramètres url
Le endpoint accepte une variable user_id de type int dans son url que nous utilisons pour le query.
Sérialisation
Si le user existe nous retournons simplement l'instance user qui sera automatiquement sérialisée selon le schéma de réponse UserSchema, retournant un payload json conforme:
{
username: "jeandupont",
first_name: "Jean",
last_name: "Dupont",
}
Documentation autogénérée
Maintenant que le schéma de réponse est défini et les paramètres statiquement typés nous bénéficions d'une api doc autogénérée pour tester le endpoint sur http://localhost:8000/api/doc
Formulaires
Nous allons maintenant examiner comment utiliser un formulaire dans un endpoint POST avec un exemple de formulaire de login. Créons d'abord des schémas pour définir les données d'entrée et de sortie:
from typing import List, Dict, Any
from ninja.schema import Schema
class LoginFormContract(Schema):
username: str | None
password: str | None
class MsgResponseContract(Schema):
message: str
class FormInvalidResponseContract(Schema):
errors: Dict[str, List[Dict[str, Any]]]
Nous allons maintenant utiliser ces schémas pour définir le endpoint:
from typing import Tuple
from django.http import HttpRequest
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import authenticate, login
from .schemas import (
LoginFormContract,
MsgResponseContract,
FormInvalidResponseContract,
)
@router.post(
"/login",
auth=None,
response={
200: None,
422: FormInvalidResponseContract,
401: MsgResponseContract,
},
)
def auth_login(
request: HttpRequest, data: LoginFormContract
) -> Tuple[int, None | FormInvalidResponseContract | MsgResponseContract]:
form = AuthenticationForm(data=data.dict())
if form.is_valid() is False:
return 422, FormInvalidResponseContract.parse_obj(
{"errors": form.errors.get_json_data(escape_html=True)}
)
user = authenticate(
username=form.cleaned_data.get("username"),
password=form.cleaned_data.get("password"),
)
if user is not None:
login(request, user) # returns a 200
return 200, None
else:
return 401, MsgResponseContract(message="Login refused")
Status codes http
Les status codes retournés par le endpoint informent le frontend du résultat du traitement du formulaire:
- 200: l'utilisateur est logué, succès
- 401: le backend refuse le login de l'utilisateur
- 422: le formulaire contient des erreurs
Erreurs du formulaire
Si le formulaire contient des erreurs, comme par exemple un champ obligatoire non rempli, nous récupérons les messages d'erreur générés par Django en json pour les transmettre au frontend
Authentification et sécurité
Il est possible de protéger l'api en la réservant aux utilisateurs authentifiés. Pour cela nous allons le déclarer dans le fichier api.py de l'app Django principale ou est déclaré le router Ninja:
from django.contrib.admin.views.decorators import staff_member_required
from ninja import NinjaAPI
from ninja.security import django_auth
from ninja.errors import ValidationError
from apps.account.api import router as account_router
api_kwargs = {
"auth": django_auth,
"csrf": True,
"docs_decorator": staff_member_required,
}
api = NinjaAPI(**api_kwargs)
api.add_router("/account/", account_router)
Auth par défaut
Spécifier le paramètre auth avec django_auth permet de réserver par défaut l'api uniquement aux utilisateurs logués. Pour faire une exception pour un endpoint, par exemple comme pour le formulaire de login ci-dessus, utiliser auth=None dans décorateur.
Csrf
Le paramètre "csrf": True indique que le frontend doit fournir un token csrf dans le header de chaque request.
Api doc
Le paramètre "docs_decorator": staff_member_required indique que la documentation de l'api est réservée aux utilisateurs staff.
Status codes et erreurs
Les status codes étant centraux, pour déterminer la nature des réponses des endpoints, nous allons ajouter un status code spécial 418 pour discriminer les erreurs de validation de schémas des autres. Les réponses 422 par exemple représentent une erreur de validation de formulaire qui peut arriver au runtime. Les erreurs de validation de schémas sont en revanche des erreurs développeurs et ne sont pas supposées arriver au runtime. Créons un validateur custom qui sera chargé de retourner un code 418: dans le fichier api.py principal:
@api.exception_handler(ValidationError)
def custom_validation_errors(
request: HttpRequest, exc: ValidationError
) -> HttpResponse:
# print the error to the terminal
print(json.dumps(exc.errors, indent=2))
# send a 418 status response with the error data
return api.create_response(request, {"detail": exc.errors}, status=418)
De cette manière, le développeur sera informé sans ambiguité si une donnée ne passe pas la validation d'un schéma.
Pour aller plus loin
Pour approfondir cet article, voir aussi:
- La documentation officielle de Django Ninja
- Le starter template django-spaninja et sa documentation