Django Ninja

Une alternative à Django REST Framework inspirée par FastAPI

Parcours pro (Django) d'un dev

Expérience / contrat DRF / Ninja ?
Software Engineer R&D en startup — site marchand DRF (découverte)
Réalise puis maintient un back (2 ans) DRF → Ninja
En entreprise, fait découvrir Ninja à un collègue DRF → Ninja (1 sous-projet)
Temps plein sur un projet (2 ans) DRF (statu quo)
Mission courte en startup Ninja (setup)
Mission longue dans un grand groupe DRF → Ninja
Django REST Framework
Django REST Framework
VS
Django Ninja
Django Ninja
DRF Ninja
Définition des routes ViewSets / Routers Décorateurs
Validation Serializers Pydantic
Async natif adrf (add-on)
Performance référence 2.1× ?
GitHub stars 30k 9k
DRF Ninja
Définition des routes ViewSets / Routers Décorateurs
DRF

            # serializers.py + views.py + urls.py
            class ItemViewSet(viewsets.ModelViewSet):
                queryset = Item.objects.all()
                serializer_class = ItemSerializer
                permission_classes = [IsAuthenticated]


            router = DefaultRouter()
            router.register(r"items", ItemViewSet)
            urlpatterns = router.urls
          
DRF

            class OrderViewSet(viewsets.ModelViewSet):
                serializer_class = OrderSerializer
                authentication_classes = [TokenAuthentication]
                permission_classes = [IsAuthenticated]

                def get_queryset(self):
                    return Order.objects.filter(owner=self.request.user)

                @action(detail=True, methods=["post"],
                        permission_classes=[IsAdminUser])
                def refund(self, request, pk=None):
                    order = self.get_object()
                    order.refund()
                    return Response({"status": "refunded"})
          
FastAPI

            app = FastAPI()


            @app.get("/")
            def read_root():
                return {"Hello": "World"}


            @app.get("/items/{item_id}")
            def read_item(item_id: int, q: Union[str, None] = None):
                return {"item_id": item_id, "q": q}
          
Django Ninja

            api = NinjaAPI()


            @api.get("/items/{item_id}")
            def read_item(request, item_id: int):
                return {"item_id": item_id}
          
Django Ninja

            # urls.py
            from django.contrib import admin
            from django.urls import path
            from ninja import NinjaAPI

            api = NinjaAPI()
            api.add_router("/items", "myapp.api.router")

            urlpatterns = [
                path("admin/", admin.site.urls),
                path("api/", api.urls),
            ]
          
DRF Ninja
Validation Serializers Pydantic
DRF

            class ItemSerializer(serializers.ModelSerializer):
                class Meta:
                    model = Item
                    fields = ["id", "name", "price"]
          
Pydantic
github.com/pydantic/pydantic
Pydantic

            from datetime import datetime
            from typing import Optional
            from pydantic import BaseModel

            class User(BaseModel):
                id: int
                name: str = 'John Doe'
                signup_ts: Optional[datetime] = None
                friends: list[int] = []
          
Pydantic

            external_data = {
                'id': '123',
                'signup_ts': '2017-06-01 12:22',
                'friends': [1, '2', b'3'],
            }
            user = User(**external_data)

            print(user)
            #> id=123 name='John Doe'
            #> signup_ts=datetime.datetime(2017, 6, 1, 12, 22)
            #> friends=[1, 2, 3]

            print(user.id)
            #> 123
          
Django Ninja

            from ninja import ModelSchema

            class ItemSchema(ModelSchema):
                class Meta:
                    model = Item
                    fields = ["id", "name", "price"]
          
DRF Ninja
Async natif adrf (add-on)

            import time

            @api.get("/say-after")
            def say_after(request, delay: int, word: str):
                time.sleep(delay)
                return {"saying": word}
    

            import asyncio

            @api.get("/say-after")
            async def say_after(request, delay: int, word: str):
                await asyncio.sleep(delay)
                return {"saying": word}
    

            def get_user():
                return UserProfile.objects.filter(
                    name="django"
                ).first()

            self.user = await sync_to_async(get_user)()

            ...

            self.user = await UserProfile.objects.filter(
                name="django"
            ).afirst()
          
fastapi.tiangolo.com/async

adrf — l'async pour DRF (add-on)

GitHub stars 611
Dernier commit il y a 7 mois
Dernière release 0.1.12 · il y a ~7 mois
Recommandé par DRF ✓ (third-party-packages)
github.com/em1208/adrf
DRF Ninja
Performance référence 2.1× ?
Parsing / validation JSON concurrency = 1, requests/sec (higher is better) 0 500 1000 1500 requests / second 1397 1336 663 sync def (gunicorn / WSGI) 549 447 280 async def (uvicorn / ASGI, DRF=adrf) Django Ninja Flask + marshmallow Django REST framework
github.com/c4ffein/django-ninja-benchmarks
Calling a slow network operation concurrency = 50 — Django Ninja uses async views 0 100 200 300 400 500 requests / second 1 2 4 6 8 10 12 14 16 18 20 22 24 number of worker processes Django Ninja — uvicorn (async) Flask + marshmallow — uWSGI (sync) Django REST framework — uWSGI (sync)
github.com/c4ffein/django-ninja-benchmarks
DRF Ninja
GitHub stars 30k 9k
GitHub stars over time: django-ninja vs django-rest-framework
au 17 juin 2026 · star-history.com
RealWorld Frontend and Backend frameworks
github.com/realworld-apps/realworld
demo.realworld.show
api.realworld.show
RealWorld — DRF
  • Découpée en 3 apps
  • KISS
  • Il y a des tests?..
RealWorld — DRF
accounts articles comments
models.py
schemas.py
serializers.py
tests.py
views.py
RealWorld — DRF

Tests existants


            def test_profile_detail_view_get(self):

                response = self.client.get(self.url)

                self.assertEqual(response.status_code, status.HTTP_200_OK)
          

            class ProfileDetailViewTestCase(APITestCase):
                def setUp(self):
                    ...

                def test_profile_detail_view_get_self(self):
                    response = self.client.get(self.url)
                    self.assertEqual(
                        response.status_code,
                        status.HTTP_200_OK,
                    )
                    self.assertEqual(loads(response.content), self.dict)
          

                def test_profile_detail_view_get_other_not_followed(self):
                    response = self.client.get(self.other_url)
                    self.assertEqual(
                        response.status_code,
                        status.HTTP_200_OK,
                    )
                    self.assertEqual(
                        loads(response.content),
                        self.other_dict,
                    )
          

                def test_profile_detail_view_get_other_followed(self):
                    self.other_user.followers.add(self.user)
                    response = self.client.get(self.other_url)
                    self.assertEqual(
                        response.status_code,
                        status.HTTP_200_OK,
                    )
                    self.assertEqual(
                        loads(response.content),
                        {**self.other_dict, 'following': True},
                    )
          
RealWorld — DRF

Views

DRF

            class ProfileDetailView(viewsets.ModelViewSet):

                queryset = User.objects.all()
                serializer_class = ProfileSerializer
                permission_classes = [IsAuthenticated]
                lookup_field = 'username'
                http_method_names = ['get', 'post', 'delete']

                def get_permissions(self):
                    if self.action == 'list':
                        return [IsAuthenticatedOrReadOnly(),]
                    return super().get_permissions()
          
DRF

                def list(self, request, username=None, *args, **kwargs):
                    try:
                        profile = User.objects.get(username=username)
                        serializer = self.get_serializer(profile)
                        return Response({"profile": serializer.data})

                    except Exception:
                        return Response({"errors": {
                            "body": [
                                "Invalid User"
                            ]
                        }})
          
DRF

            class ProfileSerializer(serializers.ModelSerializer):
                following = serializers.SerializerMethodField()

                class Meta:
                    model = User
                    fields = ('username', 'bio', 'image', 'following')
          
DRF

                def get_following(self, obj):
                    user = self.context.get('request').user
                    if user.is_authenticated:
                        return obj.followers.filter(pk=user.id).exists()
                    return False
          
Django Ninja

           @router.get(
               '/profiles/{username}',
               auth=AuthJWT(pass_even=True),
               response={
                   200: Any,
                   401: Any,
                   404: Any,
               },
           )
          
Django Ninja

           def get_profiles(request, username: str):
               user = get_object_or_404(User, username=username)
               return {
                   'username': user.username,
                   'bio': user.bio,
                   'image': user.image,
                   'following': (
                       request.user is not None
                       and request.user.is_authenticated
                       and user.followers.filter(
                           pk=request.user.id
                       ).exists()
                   )
               }
          
Django Ninja

           @router.get(
               '/profiles/{username}',
               auth=AuthJWT(pass_even=True),
               response={
                   200: ProfileSchema,
                   401: Any,
                   404: Any,
               },
           )
           def get_profiles(request, username: str):
               return ProfileSchema.from_orm(
                   get_object_or_404(User, username=username),
                   context={"request": request},
               )
          
Django Ninja

            class ProfileSchema(ModelSchema):
                following: bool

                class Meta:
                    model = User
                    fields = ["username", "bio", "image"]

                @staticmethod
                def resolve_following(obj, context) -> bool:
                    user = context.get("request").user
                    if user.is_authenticated:
                        return obj.followers.filter(pk=user.id).exists()
                    return False
          
Django Ninja

class AuthJWT(HttpBearer, JWTBaseAuthentication):
    openapi_scheme = "token"
    user_model = User

    def __init__(self, *args, pass_even=False, **kwargs):
        self.pass_even = pass_even
        super().__init__(*args, **kwargs)

    def authenticate(self, request, key):
        return (
            self.jwt_authenticate(request, token=key)
            or self.pass_even
        )
          
Django Ninja

api = NinjaAPI()
api.add_router("/", "accounts.api.router")
api.add_router("/", "comments.api.router")

...

urlpatterns = [
    path('admin/', admin.site.urls),
    ...
    path(f'{api_prefix}/', include('articles.urls')),
    path(f'{api_prefix}/', api.urls),
]