ViewSets y Routers
Un ViewSet agrupa las acciones de un recurso en una sola clase. Con un router, las URLs de esas acciones se generan automáticamente.
ModelViewSet — CRUD completo
ModelViewSet es un ViewSet con las cinco acciones CRUD ya implementadas: list, create, retrieve, update y destroy. Solo se definen queryset y serializer_class.
from rest_framework import viewsets
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.select_related("category").all()
serializer_class = ProductSerializer
permission_classes = [IsAuthenticated]
| HTTP | URL | Acción |
|---|---|---|
GET |
/products/ |
list() |
POST |
/products/ |
create() |
GET |
/products/{id}/ |
retrieve() |
PUT |
/products/{id}/ |
update() |
DELETE |
/products/{id}/ |
destroy() |
- Trade-off: Máxima productividad, pero mucha magia. Si se necesita algo fuera de lo estándar, se usa
@actiono se baja a mixins.
ReadOnlyModelViewSet — solo lectura
ReadOnlyModelViewSet es un ViewSet con únicamente las acciones de lectura: list y retrieve. No expone create, update ni destroy.
class ProductViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
- Trade-off: No expone DELETE por error, pero se necesita otro view si después se quiere crear.
Routers — URLs automáticas
Un router genera las URLs de un ViewSet a partir de sus acciones. register() vincula el ViewSet a un prefijo de URL, y include(router.urls) las agrega a urlpatterns.
from rest_framework.routers import DefaultRouter
from django.urls import path, include
router = DefaultRouter()
router.register(r"products", ProductViewSet, basename="product")
urlpatterns = [
path("api/", include(router.urls)),
]
| Router | URLs generadas | Docs view |
|---|---|---|
SimpleRouter |
Solo CRUD | No |
DefaultRouter |
CRUD + root view | Sí, en /api/ |
Siempre definir basename en el router. Así se evitan warnings y problemas con get_queryset() dinámico.
@action — endpoints custom
@action convierte un método del ViewSet en un endpoint adicional fuera del CRUD estándar: publicar, archivar, marcar como leído.
from rest_framework.decorators import action
from rest_framework.response import Response
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
@action(detail=True, methods=["post"])
def publish(self, request, pk=None):
"""POST /products/{id}/publish/"""
product = self.get_object()
product.publish()
return Response({"status": "published"})
from rest_framework.decorators import action
from rest_framework.response import Response
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
@action(detail=False, methods=["get"])
def recent(self, request):
"""GET /products/recent/"""
products = self.get_queryset().order_by("-created_at")[:5]
serializer = self.get_serializer(products, many=True)
return Response(serializer.data)
| detail=True | Opera sobre un recurso específico (requiere pk) |
| detail=False | Opera sobre la colección completa |
- Trade-off: Mucho más limpio que agregar una URL aparte, pero el action queda atado al ViewSet.
Override de métodos clave
Los ViewSets heredan get_queryset(), get_serializer_class() y perform_create() de GenericAPIView. Estos tres overrides resuelven los casos más comunes. No acumularlos si no se necesitan.
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
def get_queryset(self):
"""Filtro base que se aplica SIEMPRE."""
return Product.objects.filter(owner=self.request.user)
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
def get_serializer_class(self):
"""Serializer diferente según la acción."""
if self.action == "list":
return ProductListSerializer
return ProductDetailSerializer
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
def perform_create(self, serializer):
"""Asigna el owner automáticamente al crear."""
serializer.save(owner=self.request.user)
Casos de uso
| Para… | Usar |
|---|---|
| CRUD completo de un modelo | ModelViewSet + DefaultRouter |
| Solo lectura (list + detail) | ReadOnlyModelViewSet |
| CRUD + endpoints extra | ModelViewSet + @action |
| Agrupar lógica relacionada | ViewSet con queryset y serializer_class |
Buenas prácticas
| Práctica | Por qué |
|---|---|
Empezar con ModelViewSet, escalar si es necesario |
El 80% de los casos se resuelve con un ViewSet y un par de overrides |
Usar get_serializer_class() para distintos serializers |
Evita un ViewSet enorme con lógica condicional en el serializer |
Siempre definir basename en el router |
Evita warnings y problemas con get_queryset() dinámico |