Traditional Django Routing#

This is the 7th article in the Django Ninja series. We have reached Chapter 3 of the series.
Chapter 3 is the centerpiece of the series because it introduces the core of Django Ninja: its API features.
I have divided this chapter into three sections:
- Section 1: Routing (Router)
- Section 2: Requests (Request)
- Section 3: Responses (Response)
Chapter 3 is also the only chapter that is divided into sections.
Now, let's enter Section 1 — Routing. First, let's understand the key learning points of this section.
GitHub Example Project#
Key Points of This Section#
There are two articles in this section:
- Part 7: Routing (Part 1) — Traditional Django Routing Approach
- Part 8: Routing (Part 2) — Django Ninja Routing Configuration
Why this arrangement? Because endpoints and routing are the starting points of API requests.
Without them, your view functions cannot receive requests, let alone return responses. Therefore, routing configuration must come first as the gateway to learning API development.
You can simply think of the so-called "endpoints" as the URLs where the APIs are located.
Main Goal of This Article#
Second, routing in Django Ninja is very different from routing in traditional Django or Django REST framework (hereafter, DRF) and is closer to the style used by FastAPI and Flask.
This was the first hurdle I encountered when learning Django Ninja (after all, I had been writing DRF for 2 years 😅), so I decided to explain it in detail across two articles (Part 1 and Part 2) to help you build a solid foundation and reduce confusion.
This article focuses on introducing the configuration of traditional Django routing. Let's get started!
What Is a Router?#
A Router is an important component of a web service. It maps HTTP requests from clients (usually browsers) to the correct handler.
In Django, routing is the mechanism that determines "which request should be handled by which view."
When a client visits a specific URL (which we call an "endpoint"), the Django server finds the corresponding handler function to execute the specific logic based on this URL.
This "mapping" process is the core responsibility of routing.
Introduction to Django Routing#
Django manages routing at different levels through urls.py, organizing all endpoints with Root-Level (project-level) and App-Level Routing.
At this point, we have mentioned three key elements of Django routing:
- Root-level routing
- App-level routing
urls.py
Here is an introduction to them.
Root-Level Routing#
Root-level routing is "project-level" routing, typically located in urls.py within the Django project directory (i.e., the NinjaForum directory in the example project).
Root-level routing is mainly used to add a unified "routing prefix" to each Django app and integrate routing from all apps.
Taking this project as an example, it might look like this:
# NinjaForum/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('posts/', include('post.urls')), # Handles routing for the post app
path('users/', include('user.urls')), # Handles routing for the user app
]
The urlpatterns here is Django's root-level routing. It directs the /posts/ and /users/ paths to the app-level routing of post and user apps, serving as the unified prefix for each app's routing.
App-Level Routing#
App-level routing is routing managed by each Django app itself. It directly maps to the view functions of the app itself:
# post/urls.py
from django.urls import path
from post import views
urlpatterns = [
path('', views.get_posts)
path('<int:post_id>/', views.post_detail),
]
After combining the Root-Level and App-Level Routing, the actual endpoints of the two App-Level routes above are:
/posts/: Retrieve all posts./posts/<int:post_id>/: Retrieve details of a specific post.
This hierarchical routing structure not only makes URLs more organized but also makes the features of different apps more modular.
For example, if we want to add a "Get User Profile" API to the user app, we only need to create the corresponding route in the user app's urls.py without modifying the project-level routing configuration.
Project Architecture Diagram#
Looking at it from a bird's-eye view might make it clearer.
Regarding traditional Django routing, the directory and file structure of the entire example project is as follows (unrelated parts omitted):
├── NinjaForum
│ ├── urls.py # Project root-level routing
│ ├── ...
├── post
│ ├── urls.py # App-level routing
│ ├── ...
├── user
│ ├── urls.py # App-level routing
│ ├── ...
├── ...
This structure is very common in Django projects and is also the standard approach in DRF.
Root-Level Routing provides the global entry points, while App-Level Routing maps URLs to view functions within each app. This design makes the project architecture more modular and scalable.
Pros and Cons of Traditional Django Routing#
Django's traditional routing mechanism organizes and defines complete URL paths (endpoints) through project-level and app-level urls.py. This design has both pros and cons. Let's explore their advantages and disadvantages.
Pros: Clear List of Endpoints#
A major advantage of traditional Django routing is that all endpoints and routes are centralized in urls.py. This means developers can see all current API endpoints (app parts) at a glance, for example:
# App-level urls.py
from django.urls import path
from . import views
urlpatterns = [
path('home/', views.home, name='home'),
path('about/', views.about, name='about'),
path('contact/', views.contact, name='contact'),
]
urls.py acts like a routing directory, making it clear at a glance.
Cons: Round-Tripping Between Endpoints and View Functions#
As mentioned earlier, routing is responsible for connecting endpoints with view functions.
Each endpoint must map to a view function, and those functions are usually placed in views.py.

This introduces a problem: developers need to switch back and forth between urls.py and views.py to fully understand an endpoint and its underlying implementation logic.
This "unintuitive" nature not only increases the cognitive load during development but also makes it easy to introduce errors when modifying APIs.
In addition, as the project scale grows, the endpoint list in urls.py will become longer and longer — finding the corresponding view function becomes time-consuming and tedious, further increasing the possibility of errors.
Summary#
In this article, we took an in-depth look at traditional Django routing design and discussed its pros and cons.
This structured design makes routing management more modular, but it also causes difficulties in maintaining and reading in large projects.
Next, we will explore how Django Ninja provides a more concise routing mechanism and compare it with traditional Django routing.
The Django Ninja Way#
Django Ninja adopts a more modern way to handle routing and view functions — it closely integrates the two, providing a more intuitive way to define API endpoints. (In fact, everyone learns from Flask ☺️)
This not only significantly reduces the need to switch between different files but also improves code readability and maintainability.
In the next article, let's explore the routing mechanism of Django Ninja and see how it resolves these issues.