vintasoftware/vinta-schedule-api/create-rest-endpoint
Claude Code and agent skill by vintasoftware/vinta-schedule-api
- community
- ext:[{"type":"hook","description":"Pre/post lifecycle hooks"}]
Checking that the SKILL.md file is still in the repository.
Add a REST endpoint to the Vinta Schedule API using DRF ViewSets, the project's `VintaScheduleModelViewSet` base, organization-scoped permissions, serializer-driven virtual models, optional django-filter filtersets, and drf-spectacular OpenAPI schema export. Wires the viewset, serializer, permissions, filterset, route registration in `<app>/routes.py`, and regenerates `schema.yml`. Use for internal REST endpoints consumed by the project's frontends or first-party integrations. For external-integration data, use [create-graphql-public-query](../create-graphql-public-query/SKILL.md).
Install
npx vskill@latest install vintasoftware/vinta-schedule-api/create-rest-endpointSemi-extensibleMentions customization, but needs skill-specific knowledge. Read the extensible skills standard
SKILL.md
Create REST Endpoint
Background — load-bearing rules live in AGENTS.md: Multi-Tenancy (every tenant-scoped queryset goes through filter_by_organization), Dependency Injection (services via DI, never direct import), Django Virtual Models (serializer → virtual model when N+1 risk), Custom Managers and Querysets (no complex querysets inline).
This skill covers the DRF wire-up: viewset in <app>/views.py, serializer in <app>/serializers.py, permission in <app>/permissions.py, filterset in <app>/filtersets.py, route registration in <app>/routes.py. All routes are gathered by DefaultRouter in vinta_schedule_api/urls.py.
The OpenAPI schema export (schema.yml) is gated by a pre-commit hook + CI. After endpoint changes, regenerate the schema or the commit fails.
Decision questions
- CRUD or custom action?
- Full CRUD on a model →
ModelViewSetsubclass viaVintaScheduleModelViewSet. - Read-only listing →
ReadOnlyModelViewSet. - One-off action (POST without a model) →
APIViewor a@actionon an existing viewset.
- Which model does it expose?
- Tenant-scoped (
SingleOrganizationModelMixin) → useModel.objects.filter_by_organization(...)inget_queryset(). - Shared → standard queryset; still confirm with the team.
- What auth + permission classes? Existing pattern: per-resource permission class in
<app>/permissions.py(e.g.CalendarAvailabilityPermission) layered on top of DRF's session / JWT auth. - Does the serializer need a virtual model? If the response includes nested or aggregate data that would N+1, yes — define it in
<app>/virtual_models.pyand reference it from the serializer. - Filtering / search / ordering? Use
django-filtervia aFilterSetin<app>/filtersets.py. DRF nativefilter_backends(SearchFilter,OrderingFilter) for search / ordering. - Custom actions (`@action`)? Each requires its own
@extend_schemato populate OpenAPI correctly. - Does the endpoint expose business logic? Then services owned by DI. Inject via
Provide["service_name"]in the viewset's__init__(rare) or in the serializer's create/update.
Checklist
For a new endpoint calendar-summaries in calendar_integration:
- Define the serializer in `calendar_integration/serializers.py`:
```python import django_virtual_models as v from rest_framework import serializers
from calendar_integration.models import Calendar from calendar_integration.virtual_models import CalendarSummaryVirtualModel
class CalendarSummarySerializer(serializers.ModelSerializer): event_count = serializers.IntegerField(read_only=True) last_event_end_time = serializers.DateTimeField(read_only=True)
class Meta: model = Calendar fields = ("id", "name", "calendar_type", "event_count", "last_event_end_time") read_only_fields = fields virtual_model = CalendarSummaryVirtualModel ```
Notes:
virtual_model = ...opts intodjango-virtual-modelsfor queryset optimization.- Read-only response →
read_only_fields = fields. Write endpoints declare writable fields explicitly. - Validation on write goes in
validate()/validate_<field>methods, not in the view.
- Add the virtual model in `calendar_integration/virtual_models.py` (only when the queryset would N+1):
```python import django_virtual_models as v
from calendar_integration.models import Calendar
class CalendarSummaryVirtualModel(v.VirtualModel): class Meta: model = Calendar deferred_fields = ["description"] # don't fetch fields the serializer doesn't expose ```
- Add the permission class in `calendar_integration/permissions.py` (if a new resource class):
```python from rest_framework.permissions import BasePermission
class CalendarSummaryPermission(BasePermission): def has_permission(self, request, view) -> bool: if not request.user.is_authenticated: return False # Must belong to an organization return getattr(request.user, "organization_membership", None) is not None ```
For most cases, reuse an existing permission class — only add a new one when the resource class is genuinely new.
- Add the filterset in `calendar_integration/filtersets.py` (optional):
```python import django_filters
from calendar_integration.models import Calendar
class CalendarSummaryFilterSet(django_filters.FilterSet): calendar_type = django_filters.CharFilter(lookup_expr="iexact") has_events = django_filters.BooleanFilter(method="filter_has_events")
class Meta: model = Calendar fields = ("calendar_type",)
def filter_has_events(self, queryset, name, value): return queryset.filter(events__isnull=not value).distinct() ```
- Add the viewset in `calendar_integration/views.py`:
```python from drf_spectacular.utils import OpenApiParameter, extend_schema from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response
from calendar_integration.filtersets import CalendarSummaryFilterSet from calendar_integration.models import Calendar from calendar_integration.permissions import CalendarSummaryPermission from calendar_integration.serializers import CalendarSummarySerializer from common.utils.view_utils import VintaScheduleModelViewSet
@extend_schema(tags=["Calendar Summaries"]) class CalendarSummaryViewSet(VintaScheduleModelViewSet): """ViewSet exposing per-calendar event-count summaries."""
permission_classes = (CalendarSummaryPermission,) queryset = Calendar.objects.all() serializer_class = CalendarSummarySerializer filterset_class = CalendarSummaryFilterSet http_method_names = ("get", "head", "options") # read-only
def get_queryset(self): user = self.request.user if not user.is_authenticated: return Calendar.objects.none() org_id = user.organization_membership.organization_id return Calendar.objects.filter_by_organization(org_id).with_summary()
@extend_schema( parameters=[ OpenApiParameter(name="event_type", required=False, type=str, location="query"), ], responses={200: CalendarSummarySerializer(many=True)}, ) @action(detail=False, methods=["get"], url_path="trending") def trending(self, request): qs = self.get_queryset().order_by("-event_count")[:10] return Response(self.get_serializer(qs, many=True).data, status=status.HTTP_200_OK) ```
Notes:
VintaScheduleModelViewSet(fromcommon/utils/view_utils.py) is the project base — provides the virtual-models hook + project-wide conventions. Use it.get_queryset()hydrates the organization scope. The manager'sfilter_by_organizationraises if missing — that's the safety net.with_summary()is a queryset method on the manager, not inline annotation in the view.http_method_namesrestricts allowed verbs.@extend_schemaprovides OpenAPI metadata. Required for@actionmethods so drf-spectacular sees the param + response shape. Apply at the viewset level for tags / shared params.
- Register the route in `calendar_integration/routes.py`:
```python from common.types import RouteDict
from .views import ( ..., CalendarSummaryViewSet, )
routes: list[RouteDict] = [ ..., { "regex": r"calendar-summaries", "viewset": CalendarSummaryViewSet, "basename": "CalendarSummaries", }, ] ```
The root URL conf (vinta_schedule_api/urls.py) imports per-app routes lists and registers each with the DefaultRouter. No edit needed at the project root.
- Regenerate the OpenAPI schema:
``bash docker compose run --rm api uv run python manage.py spectacular --color --file schema.yml ``
Pre-commit hook (backend-schema in .pre-commit-config.yaml) verifies the schema is up-to-date. Run it before committing or CI fails.
- Tests in
calendar_integration/tests/test_calendar_summary_endpoint.py:
- Authenticated user with org membership receives a 200 + serialized list.
- Anonymous user receives 401.
- User of org A receives only org A's data (cross-tenant isolation).
- Filterset works:
?calendar_type=PRIMARYfilters correctly. - Custom
@action:/calendar-summaries/trending/returns the right shape. - Virtual model prevents N+1: assert query count is bounded (use
django_assert_max_num_queries).
Pitfalls
(Multi-tenancy bypass + DI bypass + complex-inline-queryset are covered upstream — see AGENTS.md → Multi-Tenancy and the reviewer agent's BLOCKER classes. Skill-specific pitfalls below.)
- Skipping `@extend_schema` on a `@action`. drf-spectacular generates a generic / wrong schema for the custom endpoint; clients break on regen.
- Forgetting `virtual_model = ...` on the serializer. Reads still N+1 against the underlying queryset.
- Not running `spectacular --file schema.yml` before commit. Pre-commit hook fails. CI fails. PR blocked.
- Permission class that doesn't check organization membership. Endpoint returns 200 for any authenticated user, including users without an org context — and
get_queryset()then crashes onuser.organization_membership.organization_idinstead of returning empty. - `http_method_names` left at default for read-only endpoints. POST / PUT / DELETE return 405, but the schema advertises them. Restrict explicitly.
- Filterset registered but not wired —
filterset_classattribute must be on the viewset, anddjango_filters.rest_framework.DjangoFilterBackendmust be in DRF'sDEFAULT_FILTER_BACKENDS. Checkvinta_schedule_api/settings/base.pybefore adding. - Custom `@action` URL collides with an existing route. Router resolves to first match; second route silently dead. Test routing.
Verification
Run the outer gate — must pass. Skill-specific extras:
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingai-tools/skills/create-rest-endpoint/SKILL.md
Popularity
- Repository stars
- 7
- 7-day trend score
- 10
Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 13 Aug 2026, 2 months ago.
Extension points
hookPre/post lifecycle hooks
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/vintasoftware/vinta-schedule-api/create-rest-endpoint)