Why I chose django-ninja instead of django-rest-framework to build coreproject

 · 10 min read
 · Baseplate-Admin
Last updated: June 19, 2023
Table of contents

django-ninja is a relatively new framework ( first commit was in July 21, 2020 ). So you might ask why would you use it instead of something like django-rest-framework ( whose first commit was all the way back in Dec 30,2011 ). Well I am glad that you asked this question. Let's talk about it.

Django Rest framework is slow

django-rest-framework is slow. Period. It's almost 2x-3x slower than django-ninja.

django-rest-framework does a whole lot of things under the hood. There are articles explaining how to improve things ( specially with serializers ) with django-rest-framework but I am have not invested enough time to understand why django-rest-framework is slow.

To me ( an average developer developing his hobby project ) serialization should be fast. I shouldn't have to invest time into a project and realize that, "Oh shit. I fucked up while picking a good stack. Alright, buckle up, Sherlock! It's time to put on your detective hat, grab your magnifying glass, and embark on a whimsically peculiar investigation."

Django Rest Framework lacks proper typing

While there's pacakges like djangorestframework-stubs, drf was born in a time when there was a major transition between Python 2 to 3. There was no concept of a strongly typed python ( let alone mypy ).

So the typing implementation for django-rest-framework often feels hacky and glued onto. Not to mention that djangorestframework-stubs is an unofficial packages.

While the maintainers of djangorestframework-stubs is undoubtly talented, their implementation is not fixing the dumpster fire that is django-rest-framework and not all the projects are using typed python ( which they should to catch bugs before runtime ).

Django Rest Framework lacks proper Swagger Support

When django-rest-framework was being actively worked on, tomchristie had another project named coreapi. django-rest-framework used to have first class support for coreapi. But @tomchristie's coreapi project never gained traction and coreapi was eventually deprecated.

You might ask why is this a big deal? Due to focusing effort on coreapi, django-rest-framework never got the same amount of love for swagger ( the de-facto openapi Swagger-compliant API ). Instead they made their own api renderer ( which is very very hard to customize properly due to how complex the autogenaration features are )

There are projects like drf-spectacular but it often requires extra effort on the developers part ( emphasizing my previous point about typing ), to achieve the functionality ( specially with @extend_schema decorator ). This implementation often feels lackluster comapred to competition.

Django rest framework's magic

django-rest-framework has a lot of ways to achieve similiar functionality. Why should this be the case. There should be one way and one right way to achieve things ( look at perl. perl thought it would be nice for everyone to have a different way to achieve same functionality. Fast forward 10 years, nobody wants to touch that unholy mess ). Look at this diagram below ( shout out to testdriven.io blog. You guys did a phenomenal job explaining things. ).

DRF diagram DRF diagram 2

Image credit : testdriven.io

I as a simple developer should not have to memorize when to use what and as a paid developer shouldn't have to worry about those who preceeded me did a good job architecting the web application.

Then there are concrete-view-classes ( I hate it ). There are countless classes ( which are just mixins ) added to GenericAPIView class. I mean whats the point? If you want people to mix and match. Just tell them to add mixins. Why are you making asumptions and making 10's ( well actually 9 ) of classes. God this irritates me so much. Wrapping your head around all these is a pain.

Django Rest Framework's lack of maintaince

All the above problems could have been overlooked if django-rest-framework was maintained properly. At the time of writing developement has recently been resumed after a long long haitus. Tom Christie's comment about things make it look bitter.

I am shocked at this specific comment :

Otherwise we should be considering the project feature-complete.

No way a project that is at the scale of django-rest-framwork is feature complete.

Why did I say this ?

  1. django-rest-framework has no way to implement nested routing. Sure this could be achieved using drf-nested-router, but i have been disappointed at the project. Since a lot of people tend to use it but finds no good example repsitory ( or at least i didn't know how to find one ). I have even raised an issue requesting this functionality
  2. django-rest-framework had over 200 open issues and pull requests ( they silence old issues with stale bot instead of properly addressing the issues at hand )
  3. django-rest-framework's bootstrap version is stuck at version 3 which was last updated at Feb 13,2019 which was about about 2 years ago since Tom Christie's comment. A feature complete library shouldn't have old dependencies. There's ongoing effort to move to bootstrap 5 but it's been stale for over 2 years. I have proposed making custom css ( which should make it feature complete ) but i don't think it will ever be implemented.

Thing like django-rest-framework is the reason ( to me atleast ) is why django is not seeing significant market share. django-rest-framework is a relic of old era. It's time to move on to the next big thing in django ecosystem. That's django-ninja.

Introducing django-ninja

django-ninja. Behold, a novel framework emerges as a shining beacon of salvation amidst the profound obscurity that envelops our current era. django-ninja gives us one and only one way to do things. So we are guranteed that projects developed using django-ninja follows a pattern.

Due to how django-ninja is developed, it emphasizes strongly on typed codebases. Which leads to good documentation along with good mypy error catching.

But django-ninja is not the silver bullet to every problem. It solves a set of problem but brings it's own set of problems.

The devil is in the details

@router.get("", response=list[AnimeInfoGETSchema])
@paginate
def get_anime_info(
    request: HttpRequest,
    filters: AnimeInfoFilters = Query(...),
) -> QuerySet[AnimeModel]:
    if not HAS_POSTGRES:
        raise Http404("Looksups are not supported on any other databases except Postgres")

    query_dict = filters.dict(exclude_none=True)
    query_object = Q()
    # 2 Step get query
    # There wont be a performance hit if we do all().filter()
    # https://docs.djangoproject.com/en/4.0/topics/db/queries/#retrieving-specific-objects-with-filters
    query = AnimeModel.objects.all()

    # We must pop this to filter other fields on the later stage
    if name := query_dict.pop("name", None):
        query = (
            query.annotate(
                similiarity=Greatest(
                    TrigramSimilarity("name", name),
                    TrigramSimilarity("name_japanese", name),
                    TrigramSimilarity("name_synonyms__name", name),
                )
            )
            .filter(
                similiarity__gte=0.3,
            )
            .order_by("-similiarity")
        )

    # Same here but with ids
    for id in [
        "mal_id",
        "kitsu_id",
        "anilist_id",
    ]:
        if value := query_dict.pop(id, None):
            _query_ = Q()
            for position in str(value).split(","):
                _query_ |= Q(
                    **{f"{id}": int(position.strip())},
                )
            query_object &= _query_

    # Staff lookups
    if staff := query_dict.pop("staffs", None):
        for position in staff.split(","):
            _query_ &= (
                Q(staff__name__icontains=position.strip())
                | Q(
                    staff__alternate_names__name__icontains=position.strip(),
                )
                | Q(staff__family_name__icontains=position.strip())
                | Q(staff__family_name__icontains=position.strip())
            )
            query_object &= _query_

    # Many to many lookups

    for item in [
        "genres",
        "themes",
        "studios",
        "producers",
        "characters",
    ]:
        if value := query_dict.pop(item, None):
            _query_ = Q()
            for position in value.split(","):
                _query_ &= Q(
                    **{f"{item}__name__icontains": position.strip()},
                )
            query_object &= _query_

    # This can be (AND: )
    # This means it is empty

    if query_object:
        query = query.filter(query_object).distinct()

    if not query:
        raise Http404(
            "No {} matches the given query with {}".format(
                query.model._meta.object_name,
                query_object,
            )
        )
    return query

See how long ( and complicated ) can a simple ( well not actually simple but detailed ) lookup function be ?

Conclusion

There's no good solution to building rest api's with django. While django-rest-framework gives us multiple ways to achieve this functionality, it suffers from "there's more than one way to achieve same thing" and django-ninja suffers from "New kid on the block" problem.

Solving these type of problem is the key to getting more marketshare for django. Otherwise new frameworks like fastapi ( which inspired starlite which became litestar | You can find the creator's reasoning in this reddit post and this medium article ) will flood the pypi registry and we will have new frameworks poping in left and right ( much like how people that were fed up with react wrote preact and now another project million is there to solve the problem both react and preact are solving ).

While django-ninja is not the silver bullet that solves every problem, it gives developers very strong base to build and extend upon. That's the main reason I chose django-ninja in hopes that it will take over django-rest-framework to become the de-facto way of building rest api's with django

If you have see any problems in this article please raise an issue the github repository