Ir para o conteúdo

ViewSets

Auto-generate CRUD endpoints from models.

Request Lifecycle

flowchart TB
    REQ[Request] --> PERM{Permission<br/>Check}
    PERM -->|Denied| R403[403 Forbidden]
    PERM -->|OK| ACTION{Action Type}
    
    ACTION -->|list| LIST[get_queryset → serialize_list]
    ACTION -->|create| CREATE[validate_data → perform_create → after_create → serialize]
    ACTION -->|retrieve| GET[get_object → serialize]
    ACTION -->|update| UPDATE[get_object → validate → perform_update]
    ACTION -->|destroy| DELETE[get_object → perform_destroy]
    
    LIST --> RESP[Response]
    CREATE --> RESP
    GET --> RESP
    UPDATE --> RESP
    DELETE --> RESP
    
    style PERM fill:#fff3e0
    style RESP fill:#c8e6c9

Basic ViewSet

from strider import ModelViewSet
from .models import Post

class PostViewSet(ModelViewSet):
    model = Post

Generated endpoints:

Method Path Action
GET /posts/ list
POST /posts/ create
GET /posts/{id} retrieve
PUT /posts/{id} update
PATCH /posts/{id} partial_update
DELETE /posts/{id} destroy

Schemas e Serializer

Controle de entrada/saída: use serializer_class (recomendado) ou input_schema/output_schema direto no ViewSet. O Serializer é o ponto único de contrato; o Router obtém os schemas dele.

from strider import ModelViewSet
from .models import Post
from .serializers import PostSerializer

class PostViewSet(ModelViewSet):
    model = Post
    serializer_class = PostSerializer

Sem Serializer:

from strider import ModelViewSet, InputSchema, OutputSchema
from .models import Post

class PostInput(InputSchema):
    title: str
    content: str
    published: bool = False

class PostOutput(OutputSchema):
    id: int
    title: str
    content: str
    published: bool
    created_at: datetime

class PostViewSet(ModelViewSet):
    model = Post
    input_schema = PostInput
    output_schema = PostOutput

Ver Serializers.

Tipagem genérica (opcional)

Nos tipos do framework, ModelT, InputT e OutputT têm default (typing_extensions.TypeVar). Na prática:

  • Pode declarar class PostViewSet(ModelViewSet): sem ModelViewSet[Post, PostInput, PostOutput] — é o estilo usado na maioria da documentação e dos templates CLI.
  • Use ModelViewSet[Post, PostInput, PostOutput] (ou ViewSet[...]) quando quiser que o type checker trate data nas actions como seu InputSchema concreto ao sobrescrever create / update / partial_update.

O contrato de API continua vindo de serializer_class ou de input_schema / output_schema; os colchetes só refinam anotações estáticas.

Inventário neste repositório

  • UnifiedModelSerializer: implementado em strider/serializers.py e exemplificado na documentação (13-serializers.md, 00-criar-aplicacao.md). Não há apps de exemplo com serializers.py próprio no monorepo (apenas o núcleo e o admin).
  • ModelViewSet sem colchetes: padrão em testes, templates em strider/cli/templates/**/views.py.template e na maior parte dos trechos em docs/.
  • ModelViewSet[ModelT, InputT, OutputT]: usado em bases reutilizáveis (SearchModelViewSet, BulkModelViewSet) e em exemplos de docstring para tipagem explícita.

Permissions

from strider import ModelViewSet
from strider.permissions import AllowAny, IsAuthenticated, IsAdminUser

class PostViewSet(ModelViewSet):
    model = Post
    
    # Default for all actions
    permission_classes = [IsAuthenticated]
    
    # Override por ação
    permission_classes_by_action = {
        "list": [AllowAny],
        "retrieve": [AllowAny],
        "create": [IsAuthenticated],
        "update": [IsAuthenticated],
        "destroy": [IsAdminUser],
    }

Custom Actions

from strider import ModelViewSet, action
from fastapi import Response

class PostViewSet(ModelViewSet):
    model = Post
    
    @action(methods=["POST"], detail=True)
    async def publish(self, request, db, **kwargs) -> dict:
        """POST /posts/{id}/publish/"""
        post = await self.get_object(db, **kwargs)
        post.published = True
        await post.save(db)
        return self._serialize_for_response(post)
    
    @action(methods=["GET"], detail=False)
    async def recent(self, request, db) -> list[dict]:
        """GET /posts/recent/"""
        qs = self.get_queryset(db)
        posts = await qs.filter(published=True).order_by("-created_at").limit(5).all()
        return self._serialize_many_for_response(posts)

Opções do @action

@action(
    methods=["POST"],
    detail=True,            # True: /posts/{id}/action/, False: /posts/action/
    url_path="custom-path", # Caminho customizado
    url_name="custom_name", # Nome da rota
    permission_classes=[IsAdminUser],
    input_schema=MyInput,   # Schema do body (opcional)
    output_schema=MyOutput, # Schema da response (opcional)
)

Automatic Route Resolution (No Manual Priority)

Custom actions are now registered with an automatic specificity sorter:

  • static paths first (/posts/list)
  • dynamic paths later (/posts/{slug})
  • wildcard/regex patterns last (/posts/{path:path})

This avoids common collisions like list being captured by a dynamic {name} route.

Optional ViewSet knobs:

class PostViewSet(ModelViewSet):
    model = Post
    # warn | raise | ignore
    route_conflict_policy = "warn"

    # Optional custom sorter:
    # (action_name, url_path, detail) -> tuple
    custom_action_sort_key = staticmethod(
        lambda action_name, url_path, detail: (0, action_name)
    )

Hooks

Hooks do ciclo de vida:

Hook Quando
perform_create_validation(data, db) Antes de instanciar o model; pode alterar data.
perform_create(instance, validated_data, db) Persistência da criação: após Model(**validated_data), antes de after_create. Recomendado incluir db e usar await instance.save(db).
after_create(obj, db) Depois de criar e salvar.
perform_update_validation(data, instance, db) Antes de atualizar.
after_update(obj, db) Depois de atualizar e salvar.

Request no ViewSet: em list, retrieve, create, update, partial_update, destroy e bulk_create, o framework define self.request, self.action e self.kwargs, para uso em hooks como perform_create_validation e perform_create.

class PostViewSet(ModelViewSet):
    model = Post
    serializer_class = PostSerializer

    async def perform_create_validation(self, data, db):
        data["author_id"] = self.request.user.id
        return data

    async def after_create(self, obj, db):
        await send_notification(f"Post {obj.id} criado")

    async def perform_update_validation(self, data, instance, db):
        data["updated_by_id"] = self.request.user.id
        return data

Assinatura legada sem db: perform_create(self, instance, validated_data) continua suportada.

QuerySet Filtering

class PostViewSet(ModelViewSet):
    model = Post
    
    def get_queryset(self, db):
        """Filter queryset based on request."""
        qs = super().get_queryset(db)
        
        # Only show user's own posts
        if not self.request.user.is_staff:
            qs = qs.filter(author_id=self.request.user.id)
        
        return qs

Paginação

class PostViewSet(ModelViewSet):
    model = Post
    page_size = 20   # padrão por página
    max_page_size = 100  # teto para page_size

Query params: ?page=1&page_size=20

Resposta:

{
  "items": [...],
  "total": 100,
  "page": 1,
  "page_size": 20,
  "pages": 5
}

Read-Only ViewSet

from strider import ReadOnlyModelViewSet

class PostViewSet(ReadOnlyModelViewSet):
    model = Post
    # Only list and retrieve, no create/update/delete

Rotas

Com auto-discovery (recomendado), crie urls.py em cada app:

# src/apps/posts/urls.py
from strider import path
from .views import PostViewSet

urlpatterns = [
    path("posts", PostViewSet),
]
# src/main.py
from strider import StrideApp

app = StrideApp()  # Carrega installed_apps e urls automaticamente

Com Router explícito:

# src/apps/posts/routes.py
from strider import Router
from .views import PostViewSet

router = Router(prefix="/posts", tags=["Posts"])
router.register_viewset("", PostViewSet)

Ver Routing.

Próximos passos