Django-Ninja: Built-in Django, Feels like FastAPI
Tags: python, django, fastapi, backend
Published: 2023-09-24 03:04:03

Django is the web framework most widely used by Python developers. Django offers a very powerful toolkit for creating web applications, making it easier for developers to create complex web applications effectively and efficiently.

However, Django has weaknesses. When it comes to performance, flexibility of project structure, compatibility with other libraries, or creating microservices, Django is not the main choice, and usually Python developers will choose to use FastAPI. Why?

FastAPI excels at building APIs and real-time applications, utilizing asynchronous programming and type hints, while Django, with its full-stack framework features, is better suited to traditional web applications with admin panels, user authentication, and more.

But, what if you could leverage the benefits of Django and get performance similar to FastAPI? I will introduce you to Django-ninja, a game-changing framework that seamlessly bridges this gap, offering web developers a versatile tool to build high-performance web APIs without compromising the power and flexibility of Django. For me, it was a combination between Django and FastAPI.

_So, what is Django-ninja?_

Django Ninja is a Django API framework that is heavily influenced by FastAPI, a popular Python web framework. It offers a variety of features, many of which are inspired by FastAPI, such as support for asynchronous programming, type-hinted and schema-based request and response validation, automatic API documentation, and a simple, intuitive, and Pythonic way to define REST API routes.

Django Ninja provides an alternative to Django REST Framework (DRF), which is another popular Django API framework. DRF is a powerful tool, but Django Ninja is a more lightweight and appealing alternative that may be a better fit for some Django projects.

For more information, you can visit their [Homepage](https://django-ninja.rest-framework.com/).

-----

In this post, I will show you how to create a simple CRUD API with Django-ninja and see how the code is just like FastAPI. To create a simple CRUD API with Django Ninja, we can follow these steps:

## Create Django Project

First, I start by creating a folder called django_ninja_example and installing Django Ninja dependencies.

``` bash
pip install django-ninja
```

And then, create a new Django project inside the folder django_ninja_example.

``` bash
django-admin startproject notes_app
```

## Create Django App

After that, we will need to create our Django app. We will create our app inside the folder notes_app. First, we need to create folder notes inside the notes_app, as follows.

```
mkdir notes_app/notes
```
Then, create the Django app using

```
django-admin startapp notes notes_app/notes
```
If you succeed, the folder structure will be like this.

```
django_ninja_example
├── manage.py
└── notes_app
    ├── __init__.py
    ├── asgi.py
    ├── notes
    │   ├── __init__.py
    │   ├── admin.py
    │   ├── apps.py
    │   ├── migrations
    │   │   └── __init__.py
    │   ├── models.py
    │   ├── tests.py
    │   └── views.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py
```

## Create a Django model

First, we need to change the `notes_app/notes/apps.py` into this.

``` python
class PagesConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'notes_app.notes'
```

And also we need to add our app into INSTALLED_APPS in `notes_app/settings.py`.

``` python
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'notes_app.notes' # this is what we need to add
]
```
Finally, we can start to create our model. We will create model Notes. Open `notes_app/notes/apps.py` and write this code.

```python
class Notes(models.Model):
    title = models.CharField(max_length=30)
    text = models.TextField()
```

Next, we will create the Schema. In traditional Django, we call it serializer. The schema structure is like what we found in FastAPI, this is because they both use Pydantic.

Create a file named schemas.py in notes_app/notes and write this code.

``` python
from ninja import Schema, ModelSchema
from notes_app.notes.models import Notes

class NoteResponseSchema(Schema):
    id: int
    title: str

class NoteDetailResponseSchema(ModelSchema):
    class Config:
        model = Notes
        model_fields = "__all__"

class NoteInputSchema(Schema):
    title: str
    text: str
```
In the code above I create three schemas. The first schema is used to get a note list, the second one is when get a single note, and the last one is used when we want to create or edit a note.

## Creating Route
For the next, let's create the API route. We will create a simple CRUD API. Let's create a file named `api.py` in the folder `notes_app/notes`, and write the code below.

``` python
from typing import List
from ninja import Router

from notes_app.notes.models import Notes
from notes_app.notes.schemas import (
    NoteResponseSchema,
    NoteDetailResponseSchema,
    NoteInputSchema,
)

router = Router()

@router.get("", response=List[NoteResponseSchema])
def view_list_notes(request):
    return Notes.objects.all()

@router.get("/{notes_id}", response=NoteDetailResponseSchema)
def view_note(request, notes_id: int):
    return Notes.objects.get(id=notes_id)

@router.post("", response=NoteDetailResponseSchema)
def create_note(request, payload: NoteInputSchema):
    note = Notes.objects.create(title=payload.title, text=payload.text)
    return note

@router.put("/{notes_id}", response=NoteDetailResponseSchema)
def edit_note(request, notes_id: int, payload: NoteInputSchema):
    note = Notes.objects.get(id=notes_id)
    note.title = payload.title
    note.text = payload.text
    note.save()
    return note

@router.delete("/{notes_id}")
def delete_note(request, notes_id: int):
    note = Notes.objects.get(id=notes_id)
    note.delete()
    return
```
As you can see it is very similar to FastAPI, the difference is it uses the Django ORM which for me personally is easier to use instead of using SQLalchemy or SQLModel.

You can also add pagination to the notes list by only using the pagination decorator. Here's an example:

``` python
from ninja.pagination import paginate

@router.get("", response=List[NoteResponseSchema])
@paginate
def view_list_notes(request):
    return Notes.objects.all()
```
You can also use an async function in Django-ninja. If you using the Django version below 4.1, you need to use the `sync_to_async()` adapter to use an async function. For example:

``` python
from asgiref.sync import sync_to_async

@router.get("/{notes_id}", response=NoteDetailResponseSchema)
async def view_note(request, notes_id: int):
    return await sync_to_async(Notes.objects.get)(id=notes_id)

@router.post("", response=NoteDetailResponseSchema)
async def create_note(request, payload: NoteInputSchema):
    note = await sync_to_async(Notes.objects.create)(
        title=payload.title, text=payload.text
    )
    return note
```
For a QuerySet result, we need to use an evaluation list. For example:

``` python
@router.get("", response=List[NoteResponseSchema])
async def view_list_notes(request):
    return await sync_to_async(list)(Notes.objects.all())
```
Django 4.1 and above come with asynchronous versions of ORM operations, so we don't need to use `sync_to_async` anymore. We just need to add a prefix to the existing synchronous operation except for the QuerySet result.

Here's an example of a simple CRUD API with asynchronous operation.

``` python
from typing import List
from ninja import Router
from ninja.pagination import paginate

from notes_app.notes.schemas import (
    NoteResponseSchema,
    NoteDetailResponseSchema,
    NoteInputSchema,
)
from notes_app.notes.models import Notes

router = Router()

@router.get("", response=List[NoteResponseSchema])
async def view_list_notes(request):
    all_blogs = [note async for note in Notes.objects.all()]
    return all_blogs

@router.get("/{notes_id}", response=NoteDetailResponseSchema)
async def view_note(request, notes_id: int):
    return await Notes.objects.aget(id=notes_id)

@router.post("", response=NoteDetailResponseSchema)
async def create_note(request, payload: NoteInputSchema):
    note = await Notes.objects.acreate(
        title=payload.title, text=payload.text
    )
    return note

@router.put("/{notes_id}", response=NoteDetailResponseSchema)
async def edit_note(request, notes_id: int, payload: NoteInputSchema):
    note = await Notes.objects.aget(id=notes_id)
    note.title = payload.title
    note.text = payload.text
    await note.asave()
    return note

@router.delete("/{notes_id}")
async def delete_note(request, notes_id: int):
    note = await Notes.objects.aget(id=notes_id)
    await note.adelete()
    return
```
If you want to add pagination to the async route, it not gonna be easy but that is possible. The Django-ninja version that I currently use is 0.22.2, which still does not support async pagination. So I'm using the [code](https://github.com/vitalik/django-ninja/issues/547#issuecomment-1331292288) that was created by [chrismaille](https://github.com/chrismaille).

I created a file for the async pagination notes_app/utils/async_pagination.py and put the code there. So I'm just adding the async pagination function to the `view_list_notes`.

``` python
from notes_app.utils.async_pagination import apaginate

@router.get("", response=List[NoteResponseSchema])
@apaginate
async def view_list_notes(request):
    all_blogs = [note async for note in Notes.objects.all()]
    return all_blogs
```
We already created the notes router. All we need is to add the notes router to the global router. Let's create file `api.py` in the notes_app folder and write this code.

``` python
from ninja import NinjaAPI
from notes_app.notes.api import router as notes_router

api = NinjaAPI()
api.add_router("notes", notes_router)
You also need to add a Django-ninja router to urls.py.
from notes_app.api import api

urlpatterns = [
    path('admin/', admin.site.urls),
    ...,
    path('api/', api.urls)
]
```
Now, everything is ready. You just need to register the notes model to the database. Write this command in the Terminal.

``` bash
python manage.py makemigrations
python manage.py migrate
```
Finally, run the code.

``` bash
python manage.py runserver
```
If the result is like the image below, then your code is successfully running with no problem.

![Terminal result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1jmg6oxtdyvnv4fn760b.png)

Then open the http://127.0.0.1/8000/api/docs or whatever host or port you use. If you open the Swagger page then you are good.

![Swagger docs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6j20fgk6vwtnwsyuz005.png)

So that's it. That is how you build Django-ninja CRUD API. If you want to read more about the Django-ninja, please read the Django-ninja documentation [here](https://django-ninja.rest-framework.com/).

---

In conclusion, Django-ninja is a powerful and versatile Python web framework for building REST APIs. It is designed to be fast, simple, and easy to use, while still providing all the features you need to build robust and scalable APIs. It has the Django system and code styling like FastAPI, it was like these two combined.

However, it also has disadvantages. Compared to the other Python frameworks, Django-ninja has a smaller community and ecosystem. It means that there are fewer third-party libraries and resources available for Django Ninja, like when we want to create an asynchronous pagination. Moreover, Django-ninja is still under development, so maybe there is still a bug or missing feature. 

That is my conclusion about Django-ninja, if you have any questions or opinions regarding Django-ninja or any other, feel free to write in the comments.