Skip to content

Django Ninja Routing#

2024 iThome Ironman Challenge

This is the 8th article in the Django Ninja series.

In the previous article, we introduced the traditional Django routing configuration.

As mentioned earlier, while having an "endpoint list" is indeed nice, as the project scale grows, constantly switching back and forth between urls.py and views.py will significantly increase the developer's cognitive load—prolonging development time and making errors more likely.

Django Ninja adopts a more modern routing design, combining design concepts from Flask and FastAPI. It not only simplifies route definitions but also improves code readability by tightly integrating routes with view functions.

Example Project Activity#

For the code changes related to routing configuration in this article, you can refer to this PR (Pull Request).

After this PR is merged, the example project officially becomes an "API project." However, it does not yet work correctly (at the point represented by this commit) because the view functions do not yet have their basic functionality.

You can follow along step-by-step with the PR of each article to learn the new content of that part. This is also my intention in creating PRs for the articles.

GitHub Example Project#

👉 Django-Ninja-Tutorial


Now, let's start introducing Django Ninja routing configuration.

Django Ninja Routing Overview#

Django Ninja uses Python decorators (decorator) to define routes and HTTP methods. This approach tightly integrates routes with view functions, greatly improving code readability.

Anyone familiar with Python will recognize that "using decorators to define routes" originated with Flask. The lightweight framework pioneered this concise and elegant design.

This design was subsequently adopted by other frameworks, such as FastAPI and Django Ninja in this article, both inheriting this flexible route definition pattern.

In Flask, developers can define routes like this:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, Flask!'

Django Ninja adopts a similar concept, except that it is syntactically more integrated into the Django ecosystem, combining type hints and Pydantic's data validation features to make API development more modern.

Below is a simple example of Django Ninja:

from ninja import NinjaAPI

api = NinjaAPI()

@api.get('/')
def hello(request):
    return {"message": "Hello, Django Ninja!"}

The Flask and Django Ninja syntax styles are not just similar, they are practically identical 😎


A More Organized Approach: Using the Router Object#

Although directly using NinjaAPI as in the example above to define routes is simple and intuitive, in actual projects, we recommend using the Router object (Official Documentation) to manage routes for different Django apps.

from ninja import Router

router = Router()  # Create a Router object

@router.get(path='/')
def hello(request):
    return {"message": "Hello, Django Ninja!"}

This aligns with the basic spirit of "separating project-level and app-level routing" in traditional Django. It not only keeps the project architecture clear but also keeps the logic of each app independent.

In Django Ninja, the Router object provides a modular routing configuration method, allowing each Django app to manage its own routes and unify them at the project-level api.py—effectively replacing the role of traditional urls.py.

In the following code examples, we will implement routing using Router objects.


Changes in Project Architecture#

Let's look at how the structure of a traditional Django project changes after adopting Django Ninja.

Traditional Django Routing Structure#

Using the example project, here is the typical structure of traditional Django:

├── NinjaForum
   ├── urls.py  # Project root-level routing
   ├── ...
├── post
   ├── urls.py  # App-level routing
   ├── view.py  # Place app-specific view functions here
   ├── ...
├── user
   ├── urls.py  # App-level routing
   ├── view.py  # Place app-specific view functions here
   ├── ...
├── ...

The app-level urls.py in Django is responsible for defining all routes within the app, which are then integrated by the project-level urls.py. It is orderly, with clear divisions of responsibilities.

Django Ninja Routing Structure#

After adopting Django Ninja, there will be some changes in the project structure. Below is a typical Django Ninja project structure:

├── NinjaForum
   ├── urls.py  # Project "level-zero" routing
   ├── api.py   # Project root-level routing
   ├── ...
├── post
   ├── api.py   # post app routing + view functions
   ├── ...
├── user
   ├── api.py   # user app routing + view functions
   ├── ...
├── ...

In this structure, each Django app has an api.py that is used to define all API routes and view functions for that app, replacing the roles of urls.py and views.py in traditional Django.

The project-level api.py is responsible for integrating APIs from all Django apps.

The project's urls.py is still necessary as a bridge that connects Django Ninja's API routes to Django's URL configuration. It can also act as "level-zero" routing, adding a shared project-wide Routing Prefix (such as /api/) to all APIs.


Implementing Django Ninja Routing#

Having understood the routing structure of Django Ninja, let's implement the "Get All Users" and "Get Post List" APIs in the two Django apps of our example project.

We will gradually improve these APIs in the next few articles. They are currently just skeletons; let's focus on the routing configuration for now.

1. Create App-Level Routing#

Create an api.py file in the user app with the following content:

# user/api.py
from ninja import Router

router = Router()

@router.get(path='/')
def get_users(request):
    users = User.objects.all()
    return users

Similarly, create a similar route and view function in post/api.py:

# post/api.py
from ninja import Router

router = Router()

@router.get(path='/')
def get_posts(request):
    posts = Post.objects.all()
    return posts

In this way, we have created APIs for both the user and post apps. Next, we need to integrate these routes into the project-level API routing.

2. Create Root-Level Routing#

We also need to create an api.py file in the Django project directory (referring to the NinjaForum directory). It will serve as our root-level routing to integrate APIs from all apps. Here is the content of this api.py:

# NinjaForum/api.py
from ninja import NinjaAPI

api = NinjaAPI()

api.add_router(prefix='/users/', router='user.api.router')
api.add_router(prefix='/posts/', router='post.api.router')

It is worth noting that there are two ways to write this routing integration. The one above is my personal preference.

Another way is to import the router object directly:

from user.api import router as user_router
from post.api import router as post_router

api.add_router(prefix='/users/', router=user_router)
api.add_router(prefix='/posts/', router=post_router)

These two methods are functionally equivalent; which one to choose depends on personal preference and project organization.

3. Project urls.py#

In Django Ninja, the project-level urls.py becomes the bridge connecting Django and Django Ninja APIs.

In the project-level urls.py, we can also define a project-wide shared routing prefix. It looks like this:

from django.contrib import admin
from django.urls import path

from NinjaForum.api import api

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', api.urls),
]

Here, a project routing prefix—/api/—is defined.

As a result, the API endpoint for "Get All Posts" will be:

/api/posts/

Of course, if you don't need an extra routing prefix, you can omit it:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', api.urls),  # Omit prefix
]

At this point, we have completed the routing configuration for Django Ninja.

This structure not only preserves Django's original modular design but also provides greater flexibility for our API development.


Wrap-Up and Next Steps#

In this first section, we learned how to define routes using Django Ninja and understood the differences between traditional Django routing and Django Ninja routing.

Django Ninja's approach to routing not only makes the code more readable but also keeps the project structure clean, improving on some of the drawbacks of traditional Django routing.

In the next article, we will enter the core part of the Django Ninja API—view functions.