Trying Out Django Ninja - Part 2

Welcome to the second part of my Django Ninja beginner tutorial. Hopefully you will have read Part 1 and now I am going to walk you through creating exactly the same app with Django REST framework. 

There are many excellent books and tutorials out there about DRF, so I dont want to get too bogged down in the 'why' of using that framework. I'll just step through how to get the same functional app as we've already built and then you can consider the differences of each approach to building REST APIs. As I've already mentioned, there is limited support for Class Based Views in Django Ninja currently, so I will use DRF's Generic Class Based views, for maximum contrast!

PART 2 - DJANGO REST FRAMEWORK.

I am going to start a new app called drf_service: 

python manage.py startapp drf_service

Next we'll install Django REST Framework:

pip install djangorestframework==3.12.2

And then go and ensure our project level settings file contains our new app and DRF:

#api_comparison/settings.py

...

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    "rest_framework",

    "ninja_service.apps.NinjaServiceConfig",
    "drf_service.apps.DrfServiceConfig",
]


....

We are going to re-use the models we created in part 1 in ninja_service, so we create a serializers.py file like so, underneath the drf_service app: 
 

#drf_service/serializers.py

from rest_framework import serializers

from ninja_service.models import Project, Release

class ProjectSerializer(serializers.ModelSerializer):
    class Meta:
        model = Project
        fields = "__all__"
        read_only_fields = (
            "id",
        )

class ReleaseSerializer(serializers.ModelSerializer):
    class Meta:
        model = Release
        fields = "__all__"
        read_only_fields = (
            "id",
        )

I suppose that it might be useful to think of Django Ninja's Schemas as roughly analogus to DRF's serializers. After all, they are taking care of parsing, validating and converting data sent to the system. However, as I mentioned in Part 1, the Schema class in Django Ninja inherits from Pydantic's BaseModel class, so its still worth familiarizing yourself with the Pydantic Docs. Here we are using the 'ModelSerializer' class, to tightly bind the serliazers to their corresponding Django Model. That doesnt quite happen in the same way in our Ninja Service. 

Next we'll move onto the views.py file in the new app: 

#drf_service/views.py

from rest_framework.generics import RetrieveUpdateDestroyAPIView, ListCreateAPIView
from ninja_service.models import Project,Release
from .serializers import ProjectSerializer,ReleaseSerializer
from rest_framework.pagination import PageNumberPagination

class CustomPagination(PageNumberPagination):
    page_size = 3

class ProjectsList(ListCreateAPIView):
    queryset = Project.objects.all()
    serializer_class = ProjectSerializer

class ProjectDetail(RetrieveUpdateDestroyAPIView):
    queryset = Project.objects.all()
    serializer_class = ProjectSerializer

class ReleaseList(ListCreateAPIView):
    queryset = Release.objects.all()
    serializer_class = ReleaseSerializer
    pagination_class = CustomPagination

class ReleaseDetail(RetrieveUpdateDestroyAPIView):
    queryset = Release.objects.all()
    serializer_class = ReleaseSerializer

Look at all the built in goodies we are importing from DRF! 

Not only are these obviously Class Based (In Django Ninja we were writing Function Based Views), but there is also far less of them. 'RetrieveUpdateDestroyAPIView' handles POST,PUT,PATCH and DELETE... that was 3 seperate functions in the other app. 

Add a urls.py file in the app: 

#drf_service/urls.py

from django.urls import path

from .views import (
ProjectDetail,
ProjectsList,
ReleaseDetail,
ReleaseList

)

urlpatterns = [
    path("projects/<int:pk>", ProjectDetail.as_view(), name="project_detail"),
    path("projects", ProjectsList.as_view(), name="project_list"),
    path("releases/<int:pk>", ReleaseDetail.as_view(), name="release_detail"),
    path("releases", ReleaseList.as_view(), name="release_list"),
]

...and then in the project level urls.py folder, expand it to point to the DRF app: 

#api_comparison/urls.py

from django.contrib import admin
from django.urls import path,include
from ninja_service.api import api
from rest_framework import permissions

urlpatterns = [
    path("admin/", admin.site.urls),
    path("ninja-api/", api.urls),
    path("drf-api/", include("drf_service.urls")),
]

I could introduce a further level of abstraction here and use DRF's routers to handle the URLs, but I actually prefer not to. Its a 'magical' step too far, for my taste. And herein lies the crux of the issue.. you dont HAVE to use the abstraction DRF provides for you. You can adopt a pick and choose approach. I am using the Generic Class Based views here for maximum contrast to Django Ninja, but you can write FBV using DRF and even create your own Base Classes, as per your requirement. 

What about the Django Ninja's auto generated docs? With DRF its not so 'out-the-box', we have to add in some extra packages and fiddle around with some config:

pip install drf-yasg==1.20.0
#api_comparison/settings.py
...

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    "rest_framework",
    'drf_yasg',#new!

    "ninja_service.apps.NinjaServiceConfig",
    "drf_service.apps.DrfServiceConfig",]

...
#api_comparison/urls.py

from django.contrib import admin
from django.urls import path,include
from ninja_service.api import api
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions


schema_view = get_schema_view(
    openapi.Info(
        title="DRF Example",
        default_version="v1",
        description="A DRF service to accompany the Django Ninja Tutorial",
        contact=openapi.Contact(email="your_name@example.com"),
    ),
    public=True,
    permission_classes=(permissions.AllowAny,),
)

urlpatterns = [
    path("admin/", admin.site.urls),
    path("ninja-api/", api.urls),
    path("drf-api/", include("drf_service.urls")),
    path(
            "drf-api/docs",
            schema_view.with_ui("swagger", cache_timeout=0),
            name="schema-swagger-ui",
        ),
]

If we take a look at the docs from Part 1, http://127.0.0.1:8000/ninja-api/docs

....and then load up http://127.0.0.1:8000/drf-api/docs

You'll see each docs page is, (more or less), the same thing, but different with regards to a couple of details. DN is reading from the Schemas we created for our app 



Where as DRF reads from the Django Models: 
 


You'll notice the authentication buttons in the DRF docs, which would appear in the DN docs, were we to be using Authentication. The point I'm trying to hammer home with this comparison is that DRF has loads more stuff already plugged into it, whereas Django Ninja, being the newer framework, needs you as the developer to be more specific and build more functionality up yourself.

That seems like as good place as any to round it off. Try all the calls you made with your Django Ninja app in Part 1, with all the same endpoints (replacing 'ninja-api' with 'drf-api' in the URIs). You will see that to the outside world they behave in much the same way, whilst giving different response bodies to the ones we constructed with Django Ninja. Perhaps you can revist the DN responses and craft something more akin to what DRF gives, if you prefer that?

An example call: 

curl -X POST "http://127.0.0.1:8000/drf-api/projects" -H  "Content-Type: application/json" -d "{"name":"Project DRF","description": "This is the first project posted via DRF"}"
{
  "id": 6,
  "name": "Project DRF",
  "description": "This is the first project posted via DRF"
}

-------------------------------------------------------------

So there you have it - the same API built with two different Django REST API Frameworks. Django Ninja is new and smaller, whilst Django REST Framework is more mature and therefore supports a lot more features. However, the main exception being Asynchronicity. This was intended to be a begginer tutorial, so I have purposefully avoided talking about async programming, but one area where DN most definately does currently have the edge over DRF is Async support.

The way Django is ordinarily taught to begginers is to initially use Function Based Views, before moving to Class Based Views and then ultimately Generic Class Based Views. The thinking behind this is that as the learner, you get a far better idea of what goes on 'under the hood' of Django before relying on higher levels of abstraction. The lack of the latter two types of view in Django Ninja, makes it the perfect framework for a new Django user who is coming to REST APIs for the first time. 

Github repository for these two tutorials. 
 

You may also like: