A router in Django REST Framework (DRF) is a class that automatically generates URL patterns for a ViewSet, wiring each action — list, create, retrieve, update, partial_update, destroy, plus any @action methods — to the correct URL and HTTP verb. Instead of hand-writing a path() entry for every endpoint, you register one ViewSet and the router emits a consistent, named URL configuration for you.
Routers are the highest level of abstraction DRF offers for URL design. They sit on top of ViewSets and class-based views, and they are the natural next step once you have read our introduction to API development with Django REST Framework. This guide is updated for Django 5.x and DRF 3.15+.
Key takeaways
- A DRF router pairs with a ViewSet to auto-generate list, detail, and custom URLs, so you stop writing repetitive
path()entries. router.register(prefix, viewset, basename=...)is the only call you need;router.urlsthen plugs straight intourlpatterns.SimpleRouterproduces the bare routes;DefaultRouteradds a browsable API root view and optional.jsonformat-suffix routes.- Every ViewSet action maps to a predictable URL, HTTP verb, and
url_name(for exampleuser-listanduser-detail). - Add extra endpoints with
@action(detail=True|False, methods=[...])— the router routes them automatically, no manual URL edits. - Always pass
basenamewhen your ViewSet overridesget_queryset()instead of declaring a staticqueryset.
What is a router in DRF, and why use one?
In a traditional Django REST API you map each function-based or class-based view to a URL by hand. That works, but for a resource with full CRUD you end up writing two views (list and detail) and several URL lines per model. Multiply that across a real project and the URL conf becomes long, repetitive, and easy to get out of sync.
A router removes that boilerplate. Combined with a ViewSet — a single class that groups the logic for a resource — a router gives you:
- Less code. One ViewSet plus one
register()call replaces multiple views and URL patterns. - Consistency. Every resource follows the same URL shape and naming convention, which makes the API predictable for clients and teammates.
- Standardised, named routes. Each route gets a
url_nameyou can pass toreverse(), so links never hard-code paths. - Faster iteration. Add a model, write its serializer and ViewSet, register it, and the endpoints exist — no URL editing.
The trade-off is a little indirection: the URLs are generated rather than spelled out, so you read the router rules instead of an explicit list. After a short learning curve that pays for itself on any non-trivial API.
# serializers.py
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'is_staff']
# views.py
from django.contrib.auth.models import User
from rest_framework import viewsets
from .serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
"""One class that handles list, create, retrieve, update and delete."""
queryset = User.objects.all()
serializer_class = UserSerializerIf serializers are new to you, see our walkthroughs on Django serializers with examples and customizing DRF serializers. The ViewSet above is a ModelViewSet, so it already provides every CRUD action — all the router has to do is expose them.
How do you register a ViewSet with a router?
Create a router instance, call register() with a URL prefix and the ViewSet, then include router.urls in your urlpatterns:
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import UserViewSet
router = DefaultRouter()
# basename is inferred from the ViewSet's queryset model -> "user"
router.register(r'users', UserViewSet)
urlpatterns = [
path('api/', include(router.urls)),
]That is the whole wiring. With the prefix users mounted under api/, the router generates /api/users/ (list and create) and /api/users/{pk}/ (retrieve, update, partial update, destroy), plus — because this is a DefaultRouter — a browsable API root at /api/ and .json format-suffix variants.
Note: Older tutorials import
urlfromdjango.conf.urlsand writeurl(r'^', include(router.urls, namespace=...)). Thedjango.conf.urls.urlhelper was removed in Django 4.0 — usedjango.urls.pathand do not passnamespacetoinclude(router.urls). The kwargbase_namewas also renamed tobasenameback in DRF 3.9.
How do ViewSets map to routes?
The router inspects which action methods the ViewSet implements and builds one URL per method, attaching the right HTTP verb and a url_name. For the users registration above, a ModelViewSet produces:
| ViewSet action | HTTP verb | Generated URL | url_name |
|---|---|---|---|
list |
GET | /users/ |
user-list |
create |
POST | /users/ |
user-list |
retrieve |
GET | /users/{pk}/ |
user-detail |
update |
PUT | /users/{pk}/ |
user-detail |
partial_update |
PATCH | /users/{pk}/ |
user-detail |
destroy |
DELETE | /users/{pk}/ |
user-detail |
Notice the pattern: routes without a {pk} (the collection URL) carry the list/create actions, while routes with a {pk} (the detail URL) carry retrieve/update/partial_update/destroy. The url_name is built as {basename}-list and {basename}-detail, and basename defaults to the lowercased model name from the ViewSet's queryset (hence user). Use those names with reverse('user-detail', args=[pk]) rather than hard-coding paths.
SimpleRouter vs DefaultRouter: which should you use?
DRF ships two main routers. DefaultRouter is a subclass of SimpleRouter that adds two conveniences: a browsable API root view that links to every registered list endpoint, and optional .json-style format suffix routes.
| Behaviour | SimpleRouter | DefaultRouter |
|---|---|---|
| API root view (index of endpoints) | No | Yes, served at the mount point |
.json / format-suffix routes |
No (opt in manually) | Yes, by default |
| Trailing slash on URLs | Yes (trailing_slash=False to disable) |
Yes (same toggle) |
| Inherits core route generation | — | Extends SimpleRouter |
| Best fit | APIs nested under a parent router, or SPA/mobile back ends that do not need the index | Standalone, browsable APIs and quick prototypes |
Reach for DefaultRouter when you want the self-documenting browsable root — great during development. Choose SimpleRouter for nested routers or when a front-end client consumes the API directly and you do not want the extra root and suffix routes. Both accept trailing_slash=False if your clients expect slash-free URLs.
How do you add custom routes with @action?
Not every endpoint fits CRUD. The @action decorator adds extra routes to a ViewSet, and the router picks them up automatically. Use detail=True for a route that operates on a single object (it gets a {pk}) and detail=False for a collection-level route:
# views.py
from django.contrib.auth.models import User
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from .serializers import PasswordSerializer, UserSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
@action(detail=True, methods=['post'])
def set_password(self, request, pk=None):
user = self.get_object()
serializer = PasswordSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user.set_password(serializer.validated_data['password'])
user.save()
return Response({'status': 'password set'}, status=status.HTTP_200_OK)
@action(detail=False)
def recent(self, request):
# GET defaults when no methods are given
users = self.get_queryset().order_by('-date_joined')[:5]
serializer = self.get_serializer(users, many=True)
return Response(serializer.data)The router now also generates:
POST /users/{pk}/set_password/withurl_name='user-set-password'GET /users/recent/withurl_name='user-recent'
By default the URL segment matches the method name (underscores kept) and the url_name uses hyphens. Override either with @action(detail=True, methods=['post'], url_path='reset-password', url_name='reset-password').
When do you need to set basename?
The router infers basename from queryset.model. If your ViewSet has no static queryset — because you compute results in get_queryset(), for example to scope rows to the current user — the router cannot infer a name and raises Cannot automatically determine the name ... please provide a 'basename'. Pass it explicitly:
# views.py
from rest_framework import viewsets
from .serializers import OrderSerializer
class OrderViewSet(viewsets.ModelViewSet):
serializer_class = OrderSerializer
def get_queryset(self):
# Dynamic queryset scoped to the logged-in user, so there is no
# static `queryset` attribute for the router to read the model from.
return self.request.user.orders.all()
# urls.py
from rest_framework.routers import DefaultRouter
from .views import OrderViewSet
router = DefaultRouter()
# basename is REQUIRED here: the ViewSet has no `queryset` attribute to infer from.
router.register(r'orders', OrderViewSet, basename='order')A good habit on any real project is to pass basename explicitly even when it could be inferred — it documents the url_name prefix and keeps reverse() calls stable if you later switch to a dynamic queryset.
How do you create nested routes?
DRF's built-in routers are flat — they do not generate parent/child URLs like /users/{user_pk}/orders/. For nested resources, install the community package drf-nested-routers:
pip install drf-nested-routers
Then nest one router inside another:
# urls.py
from django.urls import path, include
from rest_framework_nested import routers
from .views import UserViewSet, OrderViewSet
router = routers.SimpleRouter()
router.register(r'users', UserViewSet, basename='user')
# /users/{user_pk}/orders/ and /users/{user_pk}/orders/{pk}/
users_router = routers.NestedSimpleRouter(router, r'users', lookup='user')
users_router.register(r'orders', OrderViewSet, basename='user-orders')
urlpatterns = [
path('api/', include(router.urls)),
path('api/', include(users_router.urls)),
]Keep nesting one level deep where you can — deeply nested URLs get awkward to consume. Once your routes are in place, document them for consumers; see our guide on documenting API requests with Swagger / OpenAPI (the modern equivalents are drf-spectacular and DRF's built-in schema generation).
Putting it together
Routers and ViewSets are the fastest way to keep a growing DRF API consistent and maintainable. The bigger wins, though, come from getting serializers, permissions, pagination, and query performance right across the whole service. If you would like a hand designing, scaling, or reviewing a Django REST API, our Django development services team has shipped 50+ production projects and can help.
Frequently Asked Questions
What does a DRF router actually generate?
A DRF router generates a list of URL patterns from a registered ViewSet. For a ModelViewSet it produces a collection URL (/prefix/) for list and create, a detail URL (/prefix/{pk}/) for retrieve, update, partial_update, and destroy, plus one route per @action method. Each pattern is given an HTTP-verb mapping and a url_name such as prefix-list or prefix-detail. You access the result through router.urls and add it to urlpatterns.
Should I use SimpleRouter or DefaultRouter?
Use DefaultRouter when you want a browsable API root view and .json format-suffix routes — ideal for development and self-documenting APIs. Use SimpleRouter for nested routers or when a front-end/mobile client consumes the API directly and the extra root and suffix routes are unnecessary. DefaultRouter is simply a SimpleRouter with those two extras added, so the core route generation is identical.
When do I need to set basename on register()?
Set basename whenever the router cannot infer it from a static queryset. That happens when your ViewSet defines get_queryset() instead of a queryset attribute, or when two ViewSets would otherwise produce the same name. If you omit it in those cases, DRF raises an error asking you to provide a basename. The value becomes the prefix for generated names like {basename}-list and {basename}-detail.
Can a router and manual urlpatterns coexist?
Yes. router.urls is just a list of URL patterns, so you can mix it with hand-written path() entries in the same urlpatterns. Register your CRUD resources with the router and add explicit path() lines for one-off endpoints (auth, webhooks, health checks) that do not map cleanly to a ViewSet. Include the router under a prefix with path('api/', include(router.urls)) and keep other patterns alongside it.
How do I add a custom endpoint to a ViewSet?
Decorate a method with @action. Use @action(detail=True, methods=['post']) for an endpoint that targets a single object (the router adds a {pk}) or @action(detail=False) for a collection-level endpoint. The router automatically generates the route, defaulting the URL segment to the method name and the url_name to a hyphenated form. Override them with the url_path and url_name arguments when you need a different shape.
How do I create nested resource URLs with routers?
DRF's built-in SimpleRouter and DefaultRouter are flat and do not build parent/child URLs. For routes like /users/{user_pk}/orders/, install drf-nested-routers and wrap a NestedSimpleRouter around the parent router, passing the parent prefix and a lookup name. Register the child ViewSet on the nested router, then include both routers' .urls in urlpatterns. Keep nesting shallow — one level deep is usually enough.