~/TechPurAI
~/tutorials/django-rest-framework/routers
intermediate·part 10 of 22·2 min read

Routers: automatic URL generation for ViewSets

Updated Aug 16, 2026Python · Django

Part 9 ended with URL patterns mapped by hand from TaskViewSet — exactly the kind of repetition a ViewSet exists to remove, still present one layer up in the URLconf. A Router generates that mapping automatically.

DefaultRouter

python
# tasks/urls.py
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet

router = DefaultRouter()
router.register("tasks", TaskViewSet, basename="task")

urlpatterns = router.urls

router.register("tasks", TaskViewSet) replaces the entire manual .as_view({...}) block from part 9 with one line. basename="task" is what the generated URL names are built from (task-list, task-detail) — required whenever TaskViewSet.queryset doesn't make the model obvious enough for DRF to infer a name automatically, which is the common case in any real project.

What gets generated

text
GET/POST    /tasks/                  → task-list
GET/PUT/PATCH/DELETE  /tasks/<pk>/    → task-detail
POST        /tasks/<pk>/mark_complete/ → task-mark-complete

Every URL from part 9's hand-written version, plus the @action from part 9's mark_complete method — the router inspects TaskViewSet for both the six standard actions and any @action-decorated methods, and generates a matching URL pattern for each without any of them declared explicitly.

The browsable API's root view

python
# taskapi/urls.py
urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("tasks.urls")),
]

Visit http://127.0.0.1:8000/api/ and DefaultRouter (specifically — the plainer SimpleRouter doesn't do this) adds a root view listing every registered viewset as a clickable link. On a project with more than one viewset — tasks, and eventually users or categories — this becomes a genuinely useful index instead of something to remember URLs for by hand.

When to still write urls.py by hand

Not every endpoint is a resource a ViewSet fits naturally — an authentication endpoint (part 15) or a one-off aggregate view has no real "list/detail" shape. Those stay as plain path() entries alongside router.urls:

python
urlpatterns = router.urls + [
    path("stats/", views.task_stats, name="task-stats"),
]
Common mistake

Forgetting basename when a ViewSet overrides get_queryset() instead of declaring a plain queryset attribute (part 16 does exactly this for permissions). Without an inferable queryset, the router can't guess a name and raises an AssertionError at startup rather than silently misnaming routes — an explicit basename avoids it entirely.

The core CRUD API is done — list, create, retrieve, update, delete, all wired up with almost no boilerplate. Next: making sure the data going in is actually correct, beyond what field types alone can catch.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous9. ViewSets: collapsing a resource's views into one classnext →11. Serializer validation: field-level and object-level