Django REST API – Routers and URL Structure
In this post, we will organize our API URLs using Django REST Framework routers.
Routers help us automatically generate URL patterns for ViewSets and keep the API structure clean, predictable, and easier to maintain.
Why URL structure matters
A REST API should be easy to understand and predictable.
When the URL structure is clear, clients know how to interact with the API resources.
/api/projects/
/api/projects/1/
/api/tasks/
/api/tasks/1/
These endpoints are easy to read because they represent resources directly.
projects → Project resources
tasks → Task resources
A good URL structure makes the API easier to use from frontend applications, mobile apps, and other services.
Manual URL patterns
Before using routers, we connected views to URLs manually.
For example, with class-based views, the URL file could look like this:
from django.urls import path
from .views import ProjectListCreateAPIView, ProjectRetrieveUpdateDestroyAPIView
urlpatterns = [
path('projects/', ProjectListCreateAPIView.as_view(), name='project-list'),
path('projects/<int:pk>/', ProjectRetrieveUpdateDestroyAPIView.as_view(), name='project-detail'),
]
This works, but as the API grows, we need to keep adding more URL patterns manually.
projects list endpoint
projects detail endpoint
tasks list endpoint
tasks detail endpoint
users list endpoint
users detail endpoint
comments list endpoint
comments detail endpoint
This can become repetitive.
What is a router?
A router is a Django REST Framework tool that automatically generates URL patterns for ViewSets.
Instead of manually writing every route, we register a ViewSet with a router.
router.register('projects', ProjectViewSet, basename='project')
From this single line, Django REST Framework can generate the standard routes for the resource.
GET /api/projects/
POST /api/projects/
GET /api/projects/1/
PUT /api/projects/1/
PATCH /api/projects/1/
DELETE /api/projects/1/
This is why routers work so well with ViewSets.
Current ViewSets
In the previous post, we simplified our views using ModelViewSet.
Your config/projects/views.py file should look similar to this:
from rest_framework import viewsets
from .models import Project, Task
from .serializers import ProjectSerializer, TaskSerializer
class ProjectViewSet(viewsets.ModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
Each ModelViewSet contains the logic for listing, creating, retrieving, updating, and deleting resources.
list
create
retrieve
update
partial_update
destroy
Now we only need a clean way to expose these actions through URLs.
Creating a DefaultRouter
Open config/projects/urls.py and import DefaultRouter.
from rest_framework.routers import DefaultRouter
from .views import ProjectViewSet, TaskViewSet
Then create a router instance.
router = DefaultRouter()
The router will be responsible for generating the URL patterns for our ViewSets.
Registering ViewSets
Now register the ProjectViewSet and TaskViewSet with the router.
router.register('projects', ProjectViewSet, basename='project')
router.register('tasks', TaskViewSet, basename='task')
The first argument defines the URL prefix.
projects → /api/projects/
tasks → /api/tasks/
The second argument is the ViewSet that handles the resource.
The basename is used internally by Django REST Framework to generate URL names.
project-list
project-detail
task-list
task-detail
Complete urls.py file
Your config/projects/urls.py file should now look like this:
from rest_framework.routers import DefaultRouter
from .views import ProjectViewSet, TaskViewSet
router = DefaultRouter()
router.register('projects', ProjectViewSet, basename='project')
router.register('tasks', TaskViewSet, basename='task')
urlpatterns = router.urls
With this small file, the router automatically creates all standard CRUD routes for both resources.
Main project URLs
In the previous setup, we included the app URLs in the main project URL configuration.
Your config/urls.py file should include the API routes like this:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('projects.urls')),
]
This means all routes generated by the router will be available under the /api/ prefix.
/api/
For example:
/api/projects/
/api/tasks/
Generated endpoints
With the current router configuration, Django REST Framework generates the following endpoints.
GET /api/projects/ → List all projects
POST /api/projects/ → Create a project
GET /api/projects/1/ → Retrieve project with ID 1
PUT /api/projects/1/ → Fully update project with ID 1
PATCH /api/projects/1/ → Partially update project with ID 1
DELETE /api/projects/1/ → Delete project with ID 1
GET /api/tasks/ → List all tasks
POST /api/tasks/ → Create a task
GET /api/tasks/1/ → Retrieve task with ID 1
PUT /api/tasks/1/ → Fully update task with ID 1
PATCH /api/tasks/1/ → Partially update task with ID 1
DELETE /api/tasks/1/ → Delete task with ID 1
These endpoints follow a common REST structure and are easy for clients to understand.
DefaultRouter vs SimpleRouter
Django REST Framework provides different router classes.
The two most common are DefaultRouter and SimpleRouter.
SimpleRouter → Generates standard routes
DefaultRouter → Generates standard routes and adds an API root view
With DefaultRouter, when you open:
http://localhost:8000/api/
You should see a browsable API root listing the available resources.
projects: http://localhost:8000/api/projects/
tasks: http://localhost:8000/api/tasks/
This is useful while developing and testing the API.
Trailing slashes
By default, Django REST Framework routers generate URLs with trailing slashes.
/api/projects/
/api/projects/1/
This is the common Django style.
It is also possible to configure routers without trailing slashes, but in this series we will keep the default behaviour.
Use trailing slashes for consistency with Django conventions.
URL naming
The basename argument helps Django REST Framework generate URL names.
For example:
router.register('projects', ProjectViewSet, basename='project')
This creates names like:
project-list
project-detail
These names can be useful when reversing URLs in Django or when generating API documentation later.
For the tasks router:
router.register('tasks', TaskViewSet, basename='task')
Django REST Framework creates:
task-list
task-detail
Recommended API structure
A clean API structure should be consistent and resource-based.
/api/projects/
/api/projects/1/
/api/tasks/
/api/tasks/1/
Avoid mixing too many different styles in the same API.
/api/getProjects/
/api/create-task/
/api/project/delete/1/
Those URLs describe actions instead of resources.
In REST APIs, the HTTP method should describe the action, while the URL should describe the resource.
POST /api/projects/ → Create project
DELETE /api/projects/1/ → Delete project
This makes the API cleaner and more predictable.
Testing the API root
Run the development server:
python manage.py runserver
Then open the API root in your browser:
API Root: http://localhost:8000/api/
If you are using DefaultRouter, you should see links to your registered resources.
Projects: http://localhost:8000/api/projects/
Tasks: http://localhost:8000/api/tasks/
Adding more resources later
One advantage of routers is that adding new resources becomes very simple.
For example, if later we create a CommentViewSet, we only need to register it with the router.
router.register('comments', CommentViewSet, basename='comment')
The router would then generate routes such as:
/api/comments/
/api/comments/1/
This keeps the URL configuration small, even as the API grows.
What we have learned
In this post, we organized our API URLs using Django REST Framework routers.
Routers generate URL patterns automatically
ViewSets work naturally with routers
DefaultRouter adds an API root view
The /api/ prefix keeps API routes organized
Resource-based URLs are cleaner than action-based URLs
Basename is used to generate URL names
Our API now has a clean and scalable URL structure.
What comes next?
In the next post, we will protect our API by adding authentication.
We will learn how authentication works in Django REST Framework and how to make endpoints available only to authenticated users.
Django REST API: Authentication