skip to content

In Django REST Framework, what URLs and route names do SimpleRouter and DefaultRouter generate for a ViewSet, and when must you pass basename?

level: middleimportance: should knowfreq 47%

answer

  1. route table per registration
  2. list and detail name suffixes
  3. root view and format suffixes
  4. inferred from the queryset attribute

basics

~20 s

Routers generate prefix/ (name basename-list), prefix/<lookup>/ (basename-detail) and one route per @action (basename-url_name). DefaultRouter also adds an api-root view and format suffixes. Pass basename when the ViewSet has no queryset attribute to infer it from.

solid answer

~30 s

For `register("projects", ProjectViewSet)`, a `SimpleRouter` generates `projects/` for list and create, named `project-list`; `projects/<pk>/` for retrieve, update, partial update and destroy, named `project-detail`; and one pattern per `@action`, named `project-<url_name>`. It routes only the methods whose actions the ViewSet implements. `DefaultRouter` adds an API root view named `api-root` with links to every list route, plus `.json`-style format suffix patterns. The basename is inferred from the ViewSet's `queryset` class attribute as the lower-cased model name, so a ViewSet that only overrides `get_queryset()` must be registered with an explicit `basename`, and since DRF 3.15 two registrations on one router cannot share one.

code

python · 15 lines
python
from django.urls import include, path
from rest_framework import routers

from .views import ProjectViewSet

router = routers.SimpleRouter(trailing_slash=False)
# ProjectViewSet defines get_queryset() only, so basename cannot be inferred
router.register("projects", ProjectViewSet, basename="project")

urlpatterns = [
    path("api/", include(router.urls)),
]

# Generated names: project-list, project-detail, plus project-<url_name>
# for each @action; URLs: api/projects and api/projects/<pk>

go deeper

for a junior

Know the two standard route names, <basename>-list and <basename>-detail, and that DefaultRouter adds an API root.

for a middle

Explain how the route table is filtered by implemented actions, how basename is inferred from the queryset attribute, and when that inference fails.

for a senior

Treat basenames as public API used by hyperlinks and clients, restrict lookup patterns, and inspect router.urls when routes misbehave.

for a principal

Decide on router layout for a large API: one router per version or area, naming conventions, and whether suffix routes are part of the contract.

## What a router does A DRF **router** turns registered ViewSets into Django URL patterns. You call `router.register(prefix, viewset, basename=None)` for each ViewSet and put `router.urls` into your URLconf. For every registration the router walks its **route table**, keeps only the HTTP methods whose actions the ViewSet actually implements, calls `viewset.as_view(mapping, ...)`, and names each pattern. ## The routes SimpleRouter generates For `router.register("projects", ProjectViewSet)` where `ProjectViewSet` is a `ModelViewSet` over a `Project` model: | URL pattern | Methods → actions | Route name | |---|---|---| | `projects/` | GET → `list`, POST → `create` | `project-list` | | `projects/<url_path>/` | per `@action(detail=False)` | `project-<url_name>` | | `projects/<lookup>/` | GET → `retrieve`, PUT → `update`, PATCH → `partial_update`, DELETE → `destroy` | `project-detail` | | `projects/<lookup>/<url_path>/` | per `@action(detail=True)` | `project-<url_name>` | Points worth knowing: - **Only implemented actions are routed.** A `ReadOnlyModelViewSet` gets GET on both URLs and nothing else; a pattern whose mapping ends up empty is skipped altogether. - **The lookup segment** uses the ViewSet's `lookup_url_kwarg` or `lookup_field` (default `pk`) as the keyword name. Its default pattern matches anything except slashes and periods; set `lookup_value_regex` on the ViewSet to restrict it. - **Collection extra actions are listed before the detail route**, so `projects/archived/` is matched as an action before `projects/<lookup>/` is tried. ## What DefaultRouter adds `DefaultRouter` subclasses `SimpleRouter` and adds two things: 1. An **API root view** at the empty path, named `api-root`, which returns hyperlinks to each registered ViewSet's list route — handy in the browsable API. 2. **Format suffix patterns**, so `projects.json` or `projects/1.json` select a renderer by suffix. Everything else — the four route templates, naming, basename inference — is identical. Pick `SimpleRouter` when you do not want the root view or suffix URLs, for example when several routers are combined under one prefix and each would otherwise add its own root. ## Constructor options | Option | Default | Effect | |---|---|---| | `trailing_slash` | `True` | `False` generates `projects` instead of `projects/` | | `use_regex_path` | `True` | `False` builds patterns with `path()` converters instead of `re_path()` regexes; the lookup then uses the ViewSet's `lookup_value_converter` (default `str`) | ## basename: where route names come from The **basename** is the first half of every route name. When `register()` is given none, the router calls `get_default_basename()`, which reads the ViewSet's **`queryset` class attribute** and uses the lower-cased model name — `Project` becomes `project`. That inference fails in a common case: a ViewSet that defines only `get_queryset()` (to scope by the caller or annotate) and no `queryset` attribute. Registering it without `basename` fails an assertion saying the basename could not be determined because the ViewSet has no `.queryset` attribute. Pass it explicitly: `router.register("projects", ProjectViewSet, basename="project")`. Choose basenames deliberately, because they are **API surface**: - hyperlinked serializers reverse `<basename>-detail` to build `url` fields; - `self.reverse_action("archive", args=[pk])` inside a ViewSet reverses `<basename>-archive`; - tests and clients that reverse route names break when a basename changes. Since DRF 3.15, a router also refuses to register two ViewSets with the **same basename**, raising `ImproperlyConfigured`. ## Wiring the router into the URLconf `urlpatterns = router.urls` works for a dedicated API URLconf; more commonly the router's patterns are included under a prefix such as `api/`. `router.urls` is computed once and cached, and registering another ViewSet afterwards clears the cache, so register everything before the URLconf reads it. ## A worked registration Suppose a `ProjectViewSet` defines `get_queryset()` only, has an `@action(detail=True, methods=["post"])` named `archive`, and is registered on a `DefaultRouter` with `basename="project"`. The router produces, in order: - `projects/` — GET list, POST create — `project-list`; - `projects/<pk>/` — GET, PUT, PATCH, DELETE — `project-detail`; - `projects/<pk>/archive/` — POST — `project-archive`; - the empty path — `api-root`, listing `projects` with its link; - a `.json`-style suffixed twin of each pattern, from the format suffix wrapper. Forgetting `basename` here stops the URLconf from loading at all, which is the error most people meet first. ## Reading the generated table When something does not resolve, print the table rather than guessing: in `manage.py shell`, iterate over `router.urls` and print each pattern with its `name`. The output shows exactly which methods were routed, how the lookup was spelled, and which names were produced.

  • In DRF, how do you restrict a router's lookup segment to numeric ids?
    Set `lookup_value_regex = r"[0-9]+"` on the ViewSet when the router uses regex patterns (the default), or build the router with `use_regex_path=False` and set `lookup_value_converter = "int"`. The default pattern accepts anything except slashes and periods, which is why a restricted lookup also avoids clashes with collection-level extra actions.
  • When would you choose SimpleRouter over DefaultRouter in a DRF project?
    When the API root view or suffix URLs are unwanted — for instance when several routers are combined under one prefix, where each `DefaultRouter` would add its own `api-root` view and suffix variants, or when the API is not browsed through the root listing at all. The ViewSet routes themselves are identical in both.

saying these in an interview costs you the question

  • The basename defaults to the URL prefix passed to register().
  • SimpleRouter cannot route @action methods; you need DefaultRouter.
  • A ReadOnlyModelViewSet registered on a router accepts POST to create objects.
  • The router infers basename from get_queryset() when no queryset attribute exists.
  • DefaultRouter names the detail route <basename>-retrieve.