ct HTTP semantics, enforce authentication, and apply pagination. It also prevents common AI mistakes like returning raw dictionaries or omitting status codes.
Step 2: Implement the Serializer Layer
Serializers are the validation boundary. The contract dictates explicit field declaration, read-only attributes, and custom validation logic.
from rest_framework import serializers
from typing import TypedDict
from inventory.models import StockRecord
class InventoryCreatePayload(TypedDict):
sku: str
warehouse_id: int
quantity: int
location_code: str | None
batch_number: str | None
class StockSerializer(serializers.ModelSerializer):
class Meta:
model = StockRecord
fields = ["id", "sku", "warehouse_id", "quantity", "location_code", "batch_number", "updated_at"]
read_only_fields = ["id", "updated_at"]
def validate_sku(self, value: str) -> str:
if not value.isalnum():
raise serializers.ValidationError("SKU must contain only alphanumeric characters")
return value.upper()
def validate_quantity(self, value: int) -> int:
if value < 0:
raise serializers.ValidationError("Quantity cannot be negative")
return value
def create(self, validated_data: InventoryCreatePayload) -> StockRecord:
return StockRecord.objects.create(**validated_data)
Architecture rationale: TypedDict bridges Python's static typing with DRF's dynamic validation. Explicit Meta.fields prevents accidental data leakage. Custom validate_* methods run before database interaction, catching malformed input early. Returning StockRecord in create ensures type checkers understand the return type.
Step 3: Construct the Viewset with Type Safety
Viewsets orchestrate request routing, serialization, and response formatting. The contract enforces explicit HTTP status codes and authentication policies.
from rest_framework import viewsets, status
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from typing import Any
from inventory.serializers import StockSerializer
from inventory.models import StockRecord
class InventoryViewSet(viewsets.ModelViewSet):
queryset = StockRecord.objects.select_related("warehouse").all()
serializer_class = StockSerializer
authentication_classes = [SessionAuthentication]
permission_classes = [IsAuthenticatedOrReadOnly]
pagination_class = PageNumberPagination
def list(self, request: Any, *args: Any, **kwargs: Any) -> Response:
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
def create(self, request: Any, *args: Any, **kwargs: Any) -> Response:
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get_paginated_response(self, data: list[dict]) -> Response:
return Response({
"count": self.paginator.page.paginator.count,
"next": self.get_next_link(),
"previous": self.get_previous_link(),
"results": data
})
Architecture rationale: select_related prevents N+1 queries on warehouse lookups. Explicit HTTP_201_CREATED aligns with REST conventions and frontend expectations. Overriding get_paginated_response standardizes the list envelope, making it predictable for TypeScript type generation. Type hints on list and create enable static analysis tools to catch mismatches before deployment.
Step 4: Wire OpenAPI Generation & Error Envelopes
Documentation must stay synchronized with code. drf-spectacular introspects viewsets and serializers to produce accurate OpenAPI 3.0 specs. Custom exception handlers standardize error payloads.
# exceptions.py
from rest_framework.views import exception_handler
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from typing import Optional, Any
def standardized_error_handler(exc: Exception, context: dict) -> Optional[Response]:
response = exception_handler(exc, context)
if response is not None:
if isinstance(exc, ValidationError):
payload = {
"error": {
"type": "validation_failure",
"status": response.status_code,
"details": response.data
}
}
else:
payload = {
"error": {
"type": "server_error",
"status": response.status_code,
"message": str(exc)
}
}
response.data = payload
return response
# views.py (decorator example)
from drf_spectacular.utils import extend_schema, OpenApiExample
from inventory.serializers import StockSerializer
class InventoryViewSet(viewsets.ModelViewSet):
@extend_schema(
summary="Register new stock entry",
request=StockSerializer,
responses={201: StockSerializer},
examples=[
OpenApiExample(
"Standard creation",
value={"sku": "WGT-9921", "warehouse_id": 4, "quantity": 150},
request_only=True
)
]
)
def create(self, request: Any, *args: Any, **kwargs: Any) -> Response:
return super().create(request, *args, **kwargs)
Architecture rationale: The exception handler distinguishes between validation failures and unexpected server errors, returning structured payloads that frontend error boundaries can parse. @extend_schema injects human-readable context into the OpenAPI spec, improving Swagger UI usability without manual YAML maintenance.
Pitfall Guide
1. Constraint Overload in JSON Contracts
Explanation: Packing line-by-line implementation rules into the contract schema stifles AI creativity and increases prompt token usage. The model begins ignoring constraints or generating boilerplate that violates the spirit of the contract.
Fix: Limit contracts to architectural boundaries: authentication, pagination, HTTP semantics, field exposure rules, and error envelope structure. Leave implementation details to the serializer and viewset logic.
2. Serializer Field Leakage
Explanation: Using ModelSerializer without explicitly defining Meta.fields causes DRF to expose every model attribute, including internal IDs, password hashes, or audit timestamps. AI generators frequently omit this declaration.
Fix: Always declare fields and read_only_fields explicitly. Treat serializers as public APIs, not database proxies. Add a pre-commit hook that fails if fields is missing.
3. Exception Handler Swallowing Validation Errors
Explanation: A custom exception handler that unconditionally wraps response.data can corrupt DRF's native validation error structure, breaking frontend form error mapping.
Fix: Check isinstance(exc, ValidationError) before restructuring the payload. Preserve field-level error arrays for validation failures, and only wrap unexpected exceptions in a generic envelope.
4. OpenAPI Spec Drift
Explanation: Running spectacular manually means the generated schema.yml quickly diverges from the actual codebase. Frontend teams compile stale TypeScript types, causing runtime mismatches.
Fix: Integrate python manage.py spectacular --file openapi/schema.yml into your CI pipeline or pre-commit workflow. Fail builds if the generated spec differs from the committed version.
5. Type Hint Illusion
Explanation: Adding -> Response to view methods without actually returning a Response object creates a false sense of type safety. Static checkers pass, but runtime behavior remains unpredictable.
Fix: Always wrap return values in Response() or JsonResponse(). Use mypy or pyright with DRF stubs to verify that serializers and views return correctly typed objects.
Explanation: AI-generated viewsets often omit pagination, returning unbounded querysets that crash under load or exhaust memory.
Fix: Configure DEFAULT_PAGINATION_CLASS globally in settings.py, or enforce pagination_class per viewset. Always test list endpoints with datasets exceeding 10,000 records.
7. Cross-Layer Type Assumptions
Explanation: Assuming Python type hints automatically validate frontend payloads ignores the network boundary. TypeScript types generated from OpenAPI specs must be compiled and checked independently.
Fix: Add a CI step that runs openapi-typescript and executes tsc --noEmit against the generated types. Fail the pipeline if frontend types diverge from the backend contract.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal microservice with strict SLA | Schema-Driven DRF + OpenAPI generation | Guarantees contract stability, reduces cross-team debugging | Low initial setup, high long-term savings |
| Rapid prototype / hackathon | Prompt-only generation with manual review | Faster iteration, acceptable risk for throwaway code | Zero setup cost, high maintenance debt |
| Public-facing API with third-party consumers | Contract-first + automated TypeScript client generation | Ensures frontend-backend alignment, reduces support tickets | Moderate setup, prevents revenue loss from integration failures |
| Legacy DRF codebase migration | Incremental schema adoption per resource | Avoids full rewrite, isolates risk to new endpoints | Phased cost, reduces migration downtime |
Configuration Template
// api_contract_template.json
{
"contract_version": "1.0",
"resource_name": "TargetModel",
"input_requirements": {
"auth_classes": ["rest_framework.authentication.TokenAuthentication"],
"permission_classes": ["rest_framework.permissions.IsAuthenticated"],
"required_fields": ["field_a", "field_b"],
"optional_fields": ["field_c"]
},
"output_requirements": {
"serializer_class": "ModelSerializer",
"viewset_class": "ModelViewSet",
"pagination_class": "PageNumberPagination",
"http_semantics": {
"GET_list": 200,
"GET_detail": 200,
"POST": 201,
"PUT": 200,
"DELETE": 204
}
},
"enforcement_rules": [
"All methods must include return type annotations",
"Validation errors return 400 with field-level arrays",
"Never expose password or internal audit fields",
"Use select_related/prefetch_related for foreign keys"
]
}
# settings.py (DRF + Spectacular configuration)
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 50,
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'EXCEPTION_HANDLER': 'core.exceptions.standardized_error_handler',
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': 'Platform API',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
'COMPONENT_SPLIT_REQUEST': True,
'POSTPROCESSING_HOOKS': [
'drf_spectacular.contrib.postprocessing_hook.extend_schema_drivers'
],
}
Quick Start Guide
- Create the contract: Copy
api_contract_template.json, replace placeholder fields with your model's requirements, and commit it to contracts/.
- Generate the boilerplate: Prompt your AI assistant with:
"Generate DRF serializer and viewset following the contract in contracts/target_model.json. Include type hints, explicit status codes, and pagination."
- Wire OpenAPI: Add
drf-spectacular to INSTALLED_APPS, configure settings.py as shown, and run python manage.py spectacular --file openapi/schema.yml.
- Validate locally: Start the dev server, visit
/api/schema/, verify Swagger UI matches your contract, and run npx openapi-typescript openapi/schema.yml --output src/api/types.ts to generate frontend types.
- Enforce in CI: Add a pipeline step that regenerates the schema and fails if
git diff detects changes. Run tsc --noEmit against generated types to catch frontend-backend mismatches before merge.