DRF Routers and Custom Actions
Routers turn a ViewSet class into a set of URLs by inspecting the action methods on the class. They only work with ViewSet subclasses, not plain APIView/GenericAPIView.
DefaultRouter vs SimpleRouter
from rest_framework.routers import DefaultRouter, SimpleRouter
router = DefaultRouter() # adds an API root view at /, plus .json/.api format suffixes
router.register("books", BookViewSet, basename="book")
| Feature | SimpleRouter |
DefaultRouter |
|---|---|---|
| CRUD URLs from viewset | yes | yes |
API root view (/) listing all registered routes |
no | yes |
Format suffix patterns (.json, .api) |
no | yes |
Use SimpleRouter in production for cleaner URL space; DefaultRouter is convenient in dev for the browsable API root.
What register() produces
For router.register("books", BookViewSet, basename="book"):
| URL | Method | Action | URL name |
|---|---|---|---|
/books/ |
GET | list |
book-list |
/books/ |
POST | create |
book-list |
/books/{pk}/ |
GET | retrieve |
book-detail |
/books/{pk}/ |
PUT | update |
book-detail |
/books/{pk}/ |
PATCH | partial_update |
book-detail |
/books/{pk}/ |
DELETE | destroy |
book-detail |
basename is used to build URL names (reverse("book-detail", args=[1])). It’s required when the router can’t infer it from queryset (e.g. when you override get_queryset() and remove the class attribute).
Custom actions with @action
For endpoints that don’t fit CRUD: POST /books/{pk}/publish/, GET /books/recent/.
from rest_framework.decorators import action
from rest_framework.response import Response
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
@action(detail=True, methods=["post"])
def publish(self, request, pk=None):
book = self.get_object()
book.published_at = timezone.now()
book.save()
return Response({"status": "published"})
@action(detail=False) # default method is GET
def recent(self, request):
recent = self.get_queryset().order_by("-created_at")[:10]
return Response(self.get_serializer(recent, many=True).data)
| Param | Effect |
|---|---|
detail=True |
URL is /books/{pk}/<action>/; calls get_object() |
detail=False |
URL is /books/<action>/; collection-level |
methods=["post", "patch"] |
HTTP methods accepted (default ["get"]) |
url_path="mark-as-read" |
Override URL segment (default = method name) |
url_name="mark_read" |
Override URL name (default = method name with _) |
permission_classes=[...] |
Override viewset permissions for this action only |
serializer_class=... |
Override serializer for this action only |
Adding non-router URLs alongside a viewset
from rest_framework.routers import DefaultRouter
from django.urls import path, include
router = DefaultRouter()
router.register("books", BookViewSet)
urlpatterns = [
path("api/", include(router.urls)),
path("api/health/", HealthView.as_view()), # non-router
]
Manually wiring a ViewSet without a router
You don’t have to use a router. Useful when you need exotic URL shapes:
book_list = BookViewSet.as_view({"get": "list", "post": "create"})
book_detail = BookViewSet.as_view({"get": "retrieve", "put": "update", "delete": "destroy"})
urlpatterns = [
path("books/", book_list),
path("books/<int:pk>/", book_detail),
]
as_view({"http_method": "action_name"}) is what the router does internally.
Nested resources
DRF’s built-in routers don’t do nested URLs. Two options:
drf-nested-routerspackage — handles/authors/{author_pk}/books/{pk}/routing and exposes the parent pk inself.kwargs["author_pk"].- Manual — define a
BookByAuthorViewSetwithget_queryset(self)readingself.kwargs["author_pk"], register it on a separate router prefix.
# With drf-nested-routers
authors_router = DefaultRouter()
authors_router.register("authors", AuthorViewSet)
books_router = NestedDefaultRouter(authors_router, "authors", lookup="author")
books_router.register("books", BookViewSet, basename="author-books")
urlpatterns = [
path("api/", include(authors_router.urls)),
path("api/", include(books_router.urls)),
]
class BookViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Book.objects.filter(author_id=self.kwargs["author_pk"])
Common router gotchas
basenamerequired whenquerysetis missing. Routers infer the basename fromviewset.queryset.model._meta.object_name. If you overrideget_queryset()and remove the class attribute, you must passbasename=.- Order matters. Custom action URLs are registered in declaration order; if two actions resolve to the same path, the later one wins.
@action(detail=False)without a customurl_pathcollides withlist./books/and/books/recent/are fine, but/books/list/(action namedlist) shadows the built-in.as_view()on a viewset takes a method-to-action dict.BookViewSet.as_view()without args raisesTypeError.
Interview angle
- “What does a router give you that manual
path()calls don’t?” — auto-generates URL patterns and names for all six CRUD actions plus@actions; centralizes API surface area. - “How does a
@action(detail=True)URL look vsdetail=False?” —/resource/{pk}/<action>/vs/resource/<action>/.detail=Truecallsget_object()automatically. - “How do you give a custom action a different permission class?” —
permission_classes=[...]argument on@action, or overrideget_permissions()and checkself.action. - “Why does the router complain
basename argument not specified?” — your viewset has no.querysetclass attribute (onlyget_queryset()), so it can’t infer the model name. - “Nested resources — does DRF support them out of the box?” — no, use
drf-nested-routersor wire manually withself.kwargs["parent_pk"].