ViewSets y Routers10 temas

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 @action o 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/
Tip:

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

Recursos

DRF Docs: ViewSets