Viewsets¶
Viewsets group the list, retrieve, create, update, and destroy actions
for a single resource onto one class. The async viewsets keep DRF's
viewset semantics (action_map, basename, suffix, the @action
decorator) and add an async dispatch path plus action-config-aware
getters.
AsyncViewSetMixin¶
AsyncViewSetMixin is the base mixin shared by every async viewset.
It does three things.
- Replaces
as_view()with a closure that returns an async view callable. The closure binds the action map, instantiates the viewset, and awaitsself.dispatch(...). - Wraps every getter that drives dispatch with action-config awareness.
The wrapped getters consult
self.action_configs.get(self.action)and return the override when one is configured. When no override is set the call delegates to the parent getter, preserving the rest of the lookup chain. - Caches the paginator instance on
self._paginatorso repeated reads ofself.paginatordo not rebuild it.
The wrapped getters are: get_serializer_class, get_request_serializer_class, get_response_serializer_class, get_permissions, get_throttles, get_parsers, get_renderers, get_pagination_class, and get_queryset. Each follows the same lookup order: ActionConfig field -> super().get_*() -> class attribute.
get_queryset accepts three queryset shapes from an ActionConfig:
- A static QuerySet instance. Returned as-is via
.all(). - A Manager. Resolved with
manager.all(). - A callable with signature
(self) -> QuerySet. Called every request.
When no ActionConfig is set and the parent getter is missing, an
AssertionError is raised with guidance to set queryset on the class,
on the action's ActionConfig, or by overriding get_queryset().
AsyncViewSet¶
AsyncViewSet = AsyncViewSetMixin + AsyncAPIView. The class has no
default actions. Use it when the wanted actions do not map onto a
queryset, or when implementing several non-CRUD endpoints under a
single resource URL.
from rest_framework.decorators import action
from restflow.views import AsyncViewSet
class HealthViewSet(AsyncViewSet):
@action(detail=False, methods=["get"])
async def ping(self, request):
return await self.aserialized_response({"status": "ok"})
@action(detail=False, methods=["get"])
async def time(self, request):
return await self.aserialized_response({"now": timezone.now()})
AsyncGenericViewSet¶
AsyncGenericViewSet = AsyncViewSetMixin + AsyncGenericAPIView. It
adds the generic surface (aget_object, afilter_queryset,
apaginate_queryset) but provides no actions by default. Use it when
composing a viewset out of mixins manually.
from restflow.views import (
AsyncGenericViewSet,
AsyncListModelMixin,
AsyncRetrieveModelMixin,
AsyncCreateModelMixin,
)
class ProductViewSet(
AsyncCreateModelMixin,
AsyncRetrieveModelMixin,
AsyncListModelMixin,
AsyncGenericViewSet,
):
queryset = Product.objects.all()
serializer_class = ProductSerializer
pagination_class = PageNumberPagination
AsyncReadOnlyModelViewSet¶
A viewset that exposes only list and retrieve.
from restflow.views import AsyncReadOnlyModelViewSet
class ProductViewSet(AsyncReadOnlyModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [RestflowFilterBackend]
filterset_class = ProductFilterSet
pagination_class = PageNumberPagination
Pair with a router and the resource is browsable but immutable.
AsyncModelViewSet¶
The full CRUD viewset: list, retrieve, create, update, partial_update, destroy.
from restflow.views import AsyncModelViewSet
class ProductViewSet(AsyncModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [RestflowFilterBackend]
filterset_class = ProductFilterSet
pagination_class = PageNumberPagination
permission_classes = [IsAuthenticated]
This is the default starting point for a model resource. Add ActionConfig entries to override per action without subclassing.
Routing¶
Async viewsets register with DRF's DefaultRouter. The router builds
the same URL patterns as for sync viewsets; the dispatch path is the
only thing that differs.
from rest_framework.routers import DefaultRouter
from django.urls import include, path
router = DefaultRouter()
router.register(r"products", ProductViewSet, basename="product")
urlpatterns = [
path("api/", include(router.urls)),
]
The router calls as_view({"get": "list", "post": "create"}) and
similar maps under the hood. AsyncViewSetMixin.as_view returns a
csrf-exempt async view callable, which Django and DRF handle the same
way as sync views.
Custom actions¶
DRF's @action decorator works as expected; restflow's dispatch path
routes to the decorated method based on the action map the router
built from it.
from rest_framework.decorators import action
class ProductViewSet(AsyncModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
@action(detail=False, methods=["post"])
async def bulk_archive(self, request):
ser = await self.avalidated_serializer()
await Product.objects.filter(id__in=ser.validated_data["ids"]).aupdate(
is_archived=True,
)
return await self.aserialized_response({"ok": True})
@action(detail=True, methods=["post"])
async def archive(self, request, pk=None):
instance = await self.aget_object()
instance.is_archived = True
await instance.asave()
return await self.aserialized_response(instance)
A custom action's self.action attribute is the method name, so
action_configs["bulk_archive"] and action_configs["archive"] will
be picked up automatically.
For per-action overrides defined inline on the decorator -- for
example, @action(serializer_class=ArchiveInputSer) -- the resolver
falls through ActionConfig (no entry), the parent getter (which reads
the inline attribute), and finally the class attribute. The two
mechanisms can coexist on the same viewset.
Per-method handlers vs viewset actions¶
The generic views (AsyncListAPIView, AsyncListCreateAPIView, and the rest) expose handlers named after HTTP methods: get, post, put, patch, delete. Viewsets expose handlers named after actions: list, retrieve, create, update, partial_update, destroy. The router builds the HTTP-method-to-action map at registration time.
The two styles produce identical wire behaviour for the same CRUD shape, but they differ in three ways.
- Single URL vs many. A viewset registered through a router covers list, retrieve, create, update, and destroy under one register call. Per-method generic views need one URL pattern per route.
- Action awareness. Inside a viewset,
self.actionresolves to the action name like list, create, or bulk_archive. Generic views do not have an action attribute, so ActionConfig overrides do not apply. - Custom routes.
@action(detail=True/False, methods=[...])is a viewset-only decorator; the router collects the decorated methods and builds extra URL patterns for them.
For a single-resource API with standard CRUD plus a few custom endpoints, the viewset path scales better. For a small endpoint with no model lookup, a generic view (or AsyncAPIView) keeps the wiring shorter.
Hook overrides¶
The same hooks available on the generic views are available on the viewsets.
| Hook | Where to override |
|---|---|
| get_queryset | Class method (sync or async). Wins over None ActionConfig.queryset. |
| aperform_create | When the create persistence path needs side-effects. |
| aperform_update | When the update persistence path needs side-effects. |
| aperform_destroy | When destroy means archive instead of delete. |
| afilter_queryset | Rare; reach for a filter backend instead. |
| apaginate_queryset | Rare; reach for a pagination class instead. |
| acheck_permissions | Cross-cutting permission gating. |
| ahandle_exception | Customise error mapping. |
The shared rule is the same as for the generic views: prefer overriding the named hook over rewriting the action method, so the action method keeps owning the request/response contract while the hook owns the behaviour change.
Async get_queryset¶
get_queryset may stay sync, the way DRF defines it, or be redefined
as async def. The viewset's getters await the result when it is a
coroutine, so an async override slots into the existing dispatch path
without changing the contract.
class ProductViewSet(AsyncModelViewSet):
serializer_class = ProductSerializer
async def get_queryset(self):
ids = await fetch_visible_ids(self.request.user)
return Product.objects.filter(id__in=ids)
When an ActionConfig.queryset is also configured for the current
action, the ActionConfig wins (because the action-config-aware
get_queryset checks the dict first). To honour both, leave the
ActionConfig queryset as None and put the request-dependent logic in
get_queryset itself.
Nested routers¶
Restflow viewsets are compatible with the
drf-nested-routers package. The nested router builds the parent and
child URL patterns the same way it does for sync viewsets, and the
async dispatch closure works under both. Pass the parent lookup field
through to the child viewset's get_queryset to scope results.
from rest_framework_nested.routers import NestedDefaultRouter
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("products", ProductViewSet, basename="product")
products_router = NestedDefaultRouter(router, "products", lookup="product")
products_router.register("reviews", ReviewViewSet, basename="product-reviews")
urlpatterns = router.urls + products_router.urls
class ReviewViewSet(AsyncModelViewSet):
serializer_class = ReviewSerializer
queryset = Review.objects.all()
def get_queryset(self):
qs = super().get_queryset()
return qs.filter(product_id=self.kwargs["product_pk"])
The nested lookup arrives in self.kwargs and is consumed by
get_queryset to filter the child queryset before any of the action
flows run.