Django REST Framework skill for Codex

DRF endpoints scoped, paginated and N+1 free. Codex writes DRF APIs that default to IsAuthenticated, scope every queryset to the caller so other users' objects return 404, and list serializer fields explicitly. Read and write serializers are split, owners are set server-side, list views use select_related, prefetch and annotations to keep query counts flat, and tests assert both permissions and a query ceiling.

  • Backend & APIs
  • $django-rest-framework
  • 238 lines
  • 2 files
  • ~2,250 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- django-rest-framework

Installs to ~/.agents/skills/django-rest-framework/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $django-rest-framework, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds Django REST Framework APIs that are secure and fast by default - explicit serializer fields, separate read and write serializers, queryset scoping in get_queryset, object-level permissions, ViewSets with routers and custom actions, cursor pagination, django-filter, throttling, consistent exception handling, drf-spectacular schemas and APIClient tests. Use when adding DRF endpoints, fixing slow serializers or N+1 queries in list views, tightening permissions, or adding pagination and filtering to a Django API.

Use it when

  • Adding or reviewing DRF serializers, viewsets or permissions
  • A DRF list endpoint is slow or its query count grows with page size
  • Users can reach objects they should not via the API
  • An API needs pagination, filters, throttling or an OpenAPI schema

Use something else when

  • You are building server-rendered Django views, models or admin: use django-apps
  • You are designing URLs, status codes and error formats independent of DRF: use rest-api-design

How to check the result

APIClient tests show another user's object returns 404, invalid payloads return 400, and list views stay under a fixed query count; spectacular --validate passes.

The mistake it prevents

Relying on has_object_permission to protect list endpoints; it never runs for lists, so every user's rows leak unless get_queryset filters them.

~/.agents/skills/django-rest-framework/SKILL.md

--- name: django-rest-framework description: Builds Django REST Framework APIs that are secure and fast by default - explicit serializer fields, separate read and write serializers, queryset… license: MIT ---

Done well means: an endpoint returns only rows the caller may see (enforced in the queryset, not only in permissions), serializers list fields explicitly, list views run a fixed number of queries regardless of page size, every list is paginated, and the OpenAPI schema matches what the code returns.

Inspect first

  • Read AGENTS.md, then the REST_FRAMEWORK dict in settings. Note DEFAULT_PERMISSION_CLASSES (DRF’s own default is AllowAny), authentication classes, pagination, throttles, exception handler and schema class.
  • Find viewsets and serializers: rg -n "ViewSet|APIView|Serializer\)" apps.
  • Check installed helpers: django-filter, drf-spectacular, djangorestframework-simplejwt.

Project defaults

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        # or rest_framework_simplejwt.authentication.JWTAuthentication
    ],
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.CursorPagination",
    "PAGE_SIZE": 50,
    "DEFAULT_FILTER_BACKENDS": [
        "django_filters.rest_framework.DjangoFilterBackend",
        "rest_framework.filters.OrderingFilter",
    ],
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.UserRateThrottle",
        "rest_framework.throttling.AnonRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {"user": "1000/hour", "anon": "50/hour"},
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
    "EXCEPTION_HANDLER": "apps.core.api.exception_handler",
}

Secure by default: public endpoints opt out with permission_classes = [AllowAny], never the other way round.

Serializers

class OrderItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = OrderItem
        fields = ["id", "sku", "qty", "unit_price_cents"]

class OrderReadSerializer(serializers.ModelSerializer):
    items = OrderItemSerializer(many=True, read_only=True)
    customer_email = serializers.EmailField(source="customer.email", read_only=True)

    class Meta:
        model = Order
        fields = ["id", "status", "total_cents", "customer_email", "items", "created_at"]

class OrderWriteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Order
        fields = ["shipping_address", "notes"]

    def validate_notes(self, value: str) -> str:
        if len(value) > 2000:
            raise serializers.ValidationError("Too long.")
        return value
  • Always list fields explicitly. "__all__" and exclude expose new model columns the day they are added.
  • Split read and write serializers when shapes differ; it removes most read_only/write_only juggling and keeps writable surface small.
  • Never accept owner, tenant, status or price from the client. Set them in the view (serializer.save(customer=request.user)) or with HiddenField(default=CurrentUserDefault()).
  • Writable nested serializers require custom create()/update(); DRF refuses nested writes otherwise. Prefer separate endpoints for child resources.
  • SerializerMethodField that queries the DB runs once per row: an N+1 in disguise. Annotate the value in the queryset and read it as a plain field.

ViewSets

class OrderViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated, IsOrderOwnerOrStaff]
    filterset_fields = ["status"]
    ordering_fields = ["created_at", "total_cents"]
    ordering = ["-created_at"]
    http_method_names = ["get", "post", "patch", "head", "options"]   # no PUT/DELETE

    def get_queryset(self):
        qs = (Order.objects
              .select_related("customer")
              .prefetch_related("items")
              .annotate(item_count=Count("items")))
        if self.request.user.is_staff:
            return qs
        return qs.filter(customer=self.request.user)

    def get_serializer_class(self):
        return OrderWriteSerializer if self.action in {"create", "partial_update"} else OrderReadSerializer

    def perform_create(self, serializer):
        serializer.save(customer=self.request.user)

    @action(detail=True, methods=["post"])
    def cancel(self, request, pk=None):
        order = self.get_object()                  # runs object permissions
        services.cancel_order(order, by=request.user)
        return Response(OrderReadSerializer(order).data)
router = DefaultRouter()
router.register("orders", OrderViewSet, basename="order")
urlpatterns = [path("api/v1/", include(router.urls))]
NeedUse
Standard CRUD on a modelModelViewSet, trimmed with http_method_names or mixins
Read-only resourceReadOnlyModelViewSet
A verb on a resource (cancel, refund)@action(detail=True, methods=["post"])
Non-model endpoint (report, webhook)APIView or @api_view

Permissions

  • has_object_permission runs only when the view calls get_object(): retrieve, update, destroy and custom detail actions. It does not filter list views. Scoping in get_queryset is what protects lists, and it also turns other users’ ids into 404.
  • Custom actions that load objects manually (Order.objects.get(pk=pk)) skip both. Use self.get_object().
class IsOrderOwnerOrStaff(BasePermission):
    def has_object_permission(self, request, view, obj):
        return request.user.is_staff or obj.customer_id == request.user.id

Pagination and filtering

PaginationChoose when
CursorPaginationFeeds and large or fast-changing tables; requires a stable, unique-ish ordering
PageNumberPaginationAdmin-style UIs needing page numbers and totals on modest tables
LimitOffsetPaginationClients that need arbitrary offsets; slow at deep offsets
  • Set ordering_fields explicitly; leaving it unset lets clients order by any serializer field, including unindexed ones.
  • django-filter FilterSet classes for anything beyond equality on a few fields.
  • PageNumberPagination runs COUNT(*) each request; on big tables use cursors.

Errors

Return one error shape. A custom handler wraps DRF’s default:

from rest_framework.views import exception_handler as drf_handler

def exception_handler(exc, context):
    response = drf_handler(exc, context)
    if response is not None:
        response.data = {"error": {"status": response.status_code, "detail": response.data}}
    return response   # None -> Django 500; make sure it is logged

Raise ValidationError, PermissionDenied, NotFound from views and services called by views; map domain exceptions in the handler.

Schema

python manage.py spectacular --file schema.yml --validate generates and validates the OpenAPI document. Annotate custom actions and APIViews with @extend_schema so the schema matches reality; check the diff of schema.yml in review.

Testing

from rest_framework.test import APIClient

def test_user_cannot_see_others_orders(db, user, other_user_order):
    client = APIClient()
    client.force_authenticate(user)
    r = client.get(f"/api/v1/orders/{other_user_order.id}/")
    assert r.status_code == 404

def test_list_query_count(db, user, django_assert_max_num_queries, orders_with_items):
    client = APIClient(); client.force_authenticate(user)
    with django_assert_max_num_queries(5):
        assert client.get("/api/v1/orders/").status_code == 200

Per endpoint test: anonymous (401/403), another user’s object (404), invalid payload (400 with field errors), happy path, and a query-count ceiling on list views with several rows.

Verification

python manage.py check
pytest -q apps/orders          # or manage.py test
python manage.py spectacular --file /tmp/schema.yml --validate

Also hit the list endpoint as a non-staff user and confirm only their rows return.

Gotchas

  • Throttling stores counters in the Django cache; with LocMemCache and several processes each process counts separately. Use Redis.
  • partial_update with required=True fields still validates only sent fields; model constraints are the backstop.
  • Serializer UniqueTogetherValidator makes the involved fields required, surprising clients; set the owner with HiddenField so it participates in validation.
  • SessionAuthentication enforces CSRF for unsafe methods; SPA clients must send the CSRF token. Token or JWT auth does not.
  • serializer.data after save() reflects the instance; if the read shape differs, return the read serializer explicitly.

Output template

## DRF change: <summary>
- Endpoints: <method path -> serializer, permission, status codes>
- Queryset scoping: <filter applied>
- Query count on list: <n>
- Pagination/filters: <class, ordering, filter fields>
- Schema: regenerated and validated: yes/no
- Tests: <cases>

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Django REST Framework"
  short_description: "DRF endpoints scoped, paginated and N+1 free"
  default_prompt: "$django-rest-framework Add a DRF orders endpoint where users only see their own orders"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem