My opinion on Django Ninja
Posted on 2025-07-06 in Programmation
If you’re using the Django web framework (DRF) and are building APIs, you probably know about the Django Rest Framework a toolkit to build APIs in Django since more than a decade (the first 3.x version was release in 2014). It’s great and I’ve used it professionally to build several APIs. It’s also complex and showing its age when compared to what can be done in Flask or FastAPI.
Luckily, there’s a new possibility in Django’s space: django-ninja. It reached 1.0 in 2023 and was first release in 2020. It’s heavily inspired by FastAPI, relies on Pydantic for payload validation and supports async out of the box. It’s also easier to setup. That’s why I decided to use it on one of my personal projects which needed an API. After about a year, I’d like to give my opinion on it.
Please keep in mind that I only built a small API with it. I haven’t used it yet on bigger projects, so I can’t say much about this use case. I think it’s ready and mature enough and would definitely try it over good old DRF, but you may reach for someone else’s opinion if you plan to use it on a big project.
Overall, I’m pleased and very impressed by django-ninja. The biggest selling point for me is Pydantic. This library is awesome and used more and more. I already use it extensively at work. I also use it in my project to validate the input of JSON data stored in the database and to parse some JSON files. Being able to also use it for my API is a big plus for me: use cases are very similar and having only one library to learn is a breeze.
Regarding the creation of the API itself, it’s a bunch of decorated and type annotated functions. django-ninja takes care of the rest like validating POST payload or mapping GET parameters to function parameters. It even generate the documentation automatically. On that regard, it’s indeed very close to how FastAPI works. Simple and yet efficient. It seems to lack the ability to use class based views, which could be detrimental to grouping some functions.
I also like the fact that to change the HTTP response code, all you have to do is return a tuple of status_code, response like in Flask.
I’m a bit surprised it doesn’t handle more things regarding authentication. It supports Django’s auth out of the box, but it’s based on cookies. So it’s only useful to debug the API in your browser. For an API key, HTTP Bearer or HTTP Basic auth, it gives you a base class you can subclass to implement your custom method. I choose to use a JWT token passed in a header. I guess it’s not more integrated because there are so many options available and possible links with the user model that what you’d need would be there anyway.
While I’m very pleased with what django-ninja has to offer, I encountered a few issues:
I have Content Security Policy (CSP) with nonce enabled on my site thanks to django-csp. Because of this, the JS and CSS loaded by the docs couldn’t be loaded because no nonce was applied in the doc templates. I solved it by copying the swagger.html template and adding nonce="{{ request.csp_nonce }}" were the static files are loaded.
When not authenticated and targeting the API with curl, I got some CSRF error. Making the HttpBearer auth the default (ie before django_auth in the list of auth providers) solved the problem.
On some endpoints, I allow the PATCH HTTP method to be used to update only the supplied fields. Initially, I used PatchDict provided by django-ninja. Sadly, given how it’s implemented, it allowed some fields to be set to None in the payload without validation error when these fields are not nullable. This caused errors at the database level when the API tried to save these models. What I ended up doing to solve this is:
- Create a custom type named NotSet integrated in Pydantic thanks to the __get_pydantic_core_schema__ class method. It takes the underlying type as an argument to display beautiful default values in the documentation. You can view its implementation here.
- Create a custom update function that skips values set to NotSet when updating the Django model. You can view its implementation here.
- Use the type like this read_at: datetime | SkipJsonSchema[NotSet] | None = NotSet(datetime.now) in my Ninja schemas. The SkipJsonSchema prevents the type to be mentioned in the documentation since it’s a technical type users can’t use directly.
It’s not as easy as I’d hoped, but it works and I think it’s an acceptable work around, at least for my needs. There’s probably a way to make this more generic and reusable on existing schemas, but I don’t need that right now.
To conclude, I’m glad this solution exists and I think I have a cleaner, more modern and more simple way to create APIs than DRF. I had some issues and spent time in the documentation at first, but nothing weird since it’s a new library for me. From what I’ve seen and after my usage, it seems like a powerful, feature complete solution to build API with Django. I hope I’ll be able to use on a bigger project!
As usual, I’d be delighted to hear your opinion in the comments below.