247 lines
6.4 KiB
Markdown
247 lines
6.4 KiB
Markdown
# Example: Django REST API Skill
|
|
|
|
This is a complete example of a Fullstack skill for Django REST API development.
|
|
|
|
---
|
|
|
|
```yaml
|
|
---
|
|
name: django-api
|
|
description: Creates Django REST Framework endpoints with serializers, views, and URL routing. Use when building REST APIs in Django.
|
|
argument-hint: [resource-name] [fields...]
|
|
allowed-tools: Read, Write, Edit, Glob, Grep
|
|
---
|
|
|
|
# Django REST API Generator
|
|
|
|
Generate production-ready Django REST Framework endpoints with proper serializers, views, URL routing, and tests.
|
|
|
|
## When to Use
|
|
|
|
- Creating new API endpoints for a resource
|
|
- Adding CRUD operations to an existing model
|
|
- Setting up API authentication and permissions
|
|
- Implementing filtering, pagination, and search
|
|
|
|
## Prerequisites
|
|
|
|
- Django REST Framework installed (`djangorestframework`)
|
|
- Model exists or will be created
|
|
- URL configuration set up for API routes
|
|
|
|
## Instructions
|
|
|
|
### Step 1: Analyze the Model
|
|
|
|
Read the existing model or create one based on the resource name and fields provided in `$ARGUMENTS`.
|
|
|
|
### Step 2: Create Serializer
|
|
|
|
Generate a serializer in `serializers.py`:
|
|
|
|
```python
|
|
from rest_framework import serializers
|
|
from .models import ResourceName
|
|
|
|
class ResourceNameSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = ResourceName
|
|
fields = ['id', 'field1', 'field2', 'created_at', 'updated_at']
|
|
read_only_fields = ['id', 'created_at', 'updated_at']
|
|
|
|
def validate_field1(self, value):
|
|
"""Add custom validation if needed."""
|
|
return value
|
|
```
|
|
|
|
### Step 3: Create Views
|
|
|
|
Generate function-based views in `views_func.py`:
|
|
|
|
```python
|
|
from rest_framework import status
|
|
from rest_framework.decorators import api_view, permission_classes
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from drf_spectacular.utils import extend_schema
|
|
|
|
from .models import ResourceName
|
|
from .serializers import ResourceNameSerializer
|
|
|
|
|
|
@extend_schema(
|
|
request=ResourceNameSerializer,
|
|
responses={201: ResourceNameSerializer},
|
|
tags=['resource-name'],
|
|
)
|
|
@api_view(['POST'])
|
|
@permission_classes([IsAuthenticated])
|
|
def create_resource(request):
|
|
"""Create a new resource."""
|
|
serializer = ResourceNameSerializer(data=request.data)
|
|
if serializer.is_valid():
|
|
serializer.save(created_by=request.user)
|
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
@extend_schema(
|
|
responses={200: ResourceNameSerializer(many=True)},
|
|
tags=['resource-name'],
|
|
)
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
def list_resources(request):
|
|
"""List all resources for the authenticated user."""
|
|
resources = ResourceName.objects.filter(created_by=request.user)
|
|
serializer = ResourceNameSerializer(resources, many=True)
|
|
return Response(serializer.data)
|
|
|
|
|
|
@extend_schema(
|
|
responses={200: ResourceNameSerializer},
|
|
tags=['resource-name'],
|
|
)
|
|
@api_view(['GET'])
|
|
@permission_classes([IsAuthenticated])
|
|
def get_resource(request, pk):
|
|
"""Get a single resource by ID."""
|
|
try:
|
|
resource = ResourceName.objects.get(pk=pk, created_by=request.user)
|
|
except ResourceName.DoesNotExist:
|
|
return Response(
|
|
{'error': 'Resource not found'},
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
serializer = ResourceNameSerializer(resource)
|
|
return Response(serializer.data)
|
|
```
|
|
|
|
### Step 4: Configure URLs
|
|
|
|
Add URL patterns in `urls.py`:
|
|
|
|
```python
|
|
from django.urls import path
|
|
from . import views_func
|
|
|
|
urlpatterns = [
|
|
path('resources/', views_func.list_resources, name='resource-list'),
|
|
path('resources/create/', views_func.create_resource, name='resource-create'),
|
|
path('resources/<int:pk>/', views_func.get_resource, name='resource-detail'),
|
|
]
|
|
```
|
|
|
|
### Step 5: Add Tests
|
|
|
|
Generate tests in `tests/test_api.py`:
|
|
|
|
```python
|
|
import pytest
|
|
from django.urls import reverse
|
|
from rest_framework import status
|
|
from rest_framework.test import APIClient
|
|
|
|
from .factories import ResourceNameFactory, UserFactory
|
|
|
|
|
|
@pytest.fixture
|
|
def api_client():
|
|
return APIClient()
|
|
|
|
|
|
@pytest.fixture
|
|
def authenticated_client(api_client):
|
|
user = UserFactory()
|
|
api_client.force_authenticate(user=user)
|
|
return api_client, user
|
|
|
|
|
|
class TestResourceAPI:
|
|
@pytest.mark.django_db
|
|
def test_create_resource(self, authenticated_client):
|
|
client, user = authenticated_client
|
|
url = reverse('resource-create')
|
|
data = {'field1': 'value1', 'field2': 'value2'}
|
|
|
|
response = client.post(url, data)
|
|
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
assert response.data['field1'] == 'value1'
|
|
|
|
@pytest.mark.django_db
|
|
def test_list_resources(self, authenticated_client):
|
|
client, user = authenticated_client
|
|
ResourceNameFactory.create_batch(3, created_by=user)
|
|
url = reverse('resource-list')
|
|
|
|
response = client.get(url)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert len(response.data) == 3
|
|
```
|
|
|
|
## Patterns & Best Practices
|
|
|
|
### Error Response Format
|
|
|
|
Always use consistent error responses:
|
|
|
|
```python
|
|
{
|
|
"error": "Human-readable message",
|
|
"code": "MACHINE_READABLE_CODE",
|
|
"details": {} # Optional additional context
|
|
}
|
|
```
|
|
|
|
### Pagination
|
|
|
|
Use cursor pagination for large datasets:
|
|
|
|
```python
|
|
REST_FRAMEWORK = {
|
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
|
|
'PAGE_SIZE': 20,
|
|
}
|
|
```
|
|
|
|
### Filtering
|
|
|
|
Use django-filter for query parameter filtering:
|
|
|
|
```python
|
|
from django_filters import rest_framework as filters
|
|
|
|
class ResourceFilter(filters.FilterSet):
|
|
created_after = filters.DateTimeFilter(field_name='created_at', lookup_expr='gte')
|
|
|
|
class Meta:
|
|
model = ResourceName
|
|
fields = ['status', 'created_after']
|
|
```
|
|
|
|
## Common Pitfalls
|
|
|
|
- **N+1 Queries**: Use `select_related()` and `prefetch_related()` in querysets
|
|
- **Missing Permissions**: Always add `permission_classes` decorator
|
|
- **No Validation**: Add custom validation in serializer methods
|
|
- **Inconsistent Responses**: Use the same response format across all endpoints
|
|
|
|
## Verification
|
|
|
|
Test the endpoints:
|
|
|
|
```bash
|
|
# Create resource
|
|
curl -X POST http://localhost:8000/api/resources/create/ \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"field1": "value1"}'
|
|
|
|
# List resources
|
|
curl http://localhost:8000/api/resources/ \
|
|
-H "Authorization: Bearer $TOKEN"
|
|
```
|
|
```
|