10 Prompts for Django: From Models to REST API

If you’re a Django developer who uses AI daily to speed up work, you know the struggle: generic prompts give generic results. After months of trial and error with ChatGPT, Claude, and GitHub Copilot, I’ve curated 10 battle-tested prompts that actually deliver production-ready code for models, views, API endpoints, and admin customization. Each prompt includes a real usage example, code snippet, and practical tips. No fluff—just actionable, copy-paste-ready prompts that will cut your development time in half.

1. Model Design with Constraints

Prompt: "Generate a Django model for a BlogPost with fields: title (unique, max 200 chars), slug (auto-populated from title, unique), content (RichTextField), published_date (DateTimeField, auto_now_add), status (choices: draft/published/archived). Add Meta options for ordering by -published_date and verbose_name. Include a str method and a custom manager method published() that filters by status='published'."

Example: After running the prompt, you get:

from django.db import models
from django.utils.text import slugify

class BlogPost(models.Model):
    STATUS_CHOICES = [
        ('draft', 'Draft'),
        ('published', 'Published'),
        ('archived', 'Archived'),
    ]
    title = models.CharField(max_length=200, unique=True)
    slug = models.SlugField(unique=True, blank=True)
    content = models.TextField()  # Use RichTextField with django-ckeditor
    published_date = models.DateTimeField(auto_now_add=True)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')

    class Meta:
        ordering = ['-published_date']
        verbose_name = 'Blog Post'
        verbose_name_plural = 'Blog Posts'

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

    @classmethod
    def published(cls):
        return cls.objects.filter(status='published')

Tip: Always specify field types and constraints explicitly. AI often forgets auto_now_add or unique if not mentioned.

2. Class-Based Views with Mixins

Prompt: "Create a Django class-based view for listing BlogPost objects with pagination (10 per page), search by title, and filtering by status. Use ListView with a custom QuerySet method. Add a mixin for caching the query for 5 minutes."

Example: The AI generates a view that combines ListView with a caching mixin:

from django.views.generic import ListView
from django.core.cache import cache
from .models import BlogPost

class CachedListViewMixin:
    def get_queryset(self):
        cache_key = f'blog_list_{self.request.GET.urlencode()}'
        qs = cache.get(cache_key)
        if qs is None:
            qs = super().get_queryset()
            cache.set(cache_key, qs, 300)  # 5 minutes
        return qs

class BlogPostListView(CachedListViewMixin, ListView):
    model = BlogPost
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    paginate_by = 10

    def get_queryset(self):
        qs = BlogPost.published()
        search = self.request.GET.get('q')
        status = self.request.GET.get('status')
        if search:
            qs = qs.filter(title__icontains=search)
        if status:
            qs = qs.filter(status=status)
        return qs

Tip: Mixins are a Django superpower. Ask AI to generate reusable mixins for caching, permission checks, or pagination—they save hours.

3. DRF API Endpoint with Serializers

Prompt: "Write a Django REST Framework API for BlogPost with a ModelSerializer that includes all fields except content (exclude it). Add a ListCreateAPIView and a RetrieveUpdateDestroyAPIView. Use a custom permission class that allows only authenticated users to create/update, but anyone can read. Include throttling for anonymous users: 100 requests/day."

Example: The AI outputs:

from rest_framework import serializers, viewsets, permissions, throttling
from .models import BlogPost

class BlogPostSerializer(serializers.ModelSerializer):
    class Meta:
        model = BlogPost
        fields = ['id', 'title', 'slug', 'published_date', 'status']

class IsAuthenticatedOrReadOnly(permissions.BasePermission):
    def has_permission(self, request, view):
        if request.method in permissions.SAFE_METHODS:
            return True
        return request.user and request.user.is_authenticated

class BlogPostViewSet(viewsets.ModelViewSet):
    queryset = BlogPost.published()
    serializer_class = BlogPostSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    throttle_classes = [throttling.AnonRateThrottle]
    throttle_rates = {'anon': '100/day'}

Tip: Specify throttle_classes and permission_classes explicitly. The AI often defaults to unrestricted access—always ask for security.

4. Admin Customization with Actions

Prompt: "Customize the Django admin for BlogPost: list display with title, status, and published_date. Add a search field for title, list filter for status. Create a custom admin action 'mark as published' that sets status to published and logs the action. Also add a custom admin template for the change form to show a preview link."

Example: The generated admin.py:

from django.contrib import admin
from .models import BlogPost

def mark_as_published(modeladmin, request, queryset):
    updated = queryset.update(status='published')
    modeladmin.message_user(request, f'{updated} posts marked as published.')
mark_as_published.short_description = 'Mark selected posts as published'

class BlogPostAdmin(admin.ModelAdmin):
    list_display = ('title', 'status', 'published_date')
    search_fields = ('title',)
    list_filter = ('status',)
    actions = [mark_as_published]

    def change_view(self, request, object_id, form_url='', extra_context=None):
        extra_context = extra_context or {}
        obj = self.get_object(request, object_id)
        if obj:
            extra_context['preview_url'] = f'/blog/{obj.slug}/'
        return super().change_view(request, object_id, form_url, extra_context)

admin.site.register(BlogPost, BlogPostAdmin)

Tip: For admin actions, always ask for a message_user call—otherwise the user sees no feedback.

5. Signals for Data Integrity

Prompt: "Create a Django signal that automatically generates a slug from the title when a BlogPost is created or updated, but only if the slug is empty. Also add a post_save signal that sends an email notification to the admin when a new post is published. Use Django's built-in signals and mail functions."

Example: The AI creates signals.py:

from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver
from django.core.mail import send_mail
from django.utils.text import slugify
from .models import BlogPost

@receiver(pre_save, sender=BlogPost)
def auto_slug(sender, instance, **kwargs):
    if not instance.slug:
        instance.slug = slugify(instance.title)

@receiver(post_save, sender=BlogPost)
def notify_admin(sender, instance, created, **kwargs):
    if created and instance.status == 'published':
        send_mail(
            'New Blog Post Published',
            f'Post "{instance.title}" has been published.',
            'from@example.com',
            ['admin@example.com'],
            fail_silently=False,
        )

Tip: In production, use a task queue (Celery) for email sending. Ask the AI to add a comment about Celery integration for high-traffic sites.

6. Query Optimization with select_related/prefetch_related

Prompt: "Optimize the BlogPost ListView query for a blog that also has an Author model (ForeignKey). Use select_related for author and prefetch_related for tags (ManyToManyField). Also add a custom QuerySet method that annotates each post with the comment count. Assume Comment model with ForeignKey to BlogPost."

Example: The AI suggests:

from django.db import models

class BlogPostQuerySet(models.QuerySet):
    def with_comment_count(self):
        return self.annotate(comment_count=models.Count('comments'))

class BlogPost(models.Model):
    author = models.ForeignKey('Author', on_delete=models.CASCADE)
    tags = models.ManyToManyField('Tag')
    # ... other fields
    objects = BlogPostQuerySet.as_manager()

# In the view:
class BlogPostListView(ListView):
    model = BlogPost
    def get_queryset(self):
        return super().get_queryset().select_related('author').prefetch_related('tags').with_comment_count()

Tip: Always ask for select_related on ForeignKey and prefetch_related on ManyToManyField. The AI might omit them, causing N+1 queries.

7. Custom Middleware for Logging

Prompt: "Write a Django middleware that logs every request to the blog app: URL, user (if authenticated), timestamp, and response status code. Use Python's logging module. Exclude static files and admin URLs. Store logs in a file called 'api_requests.log'."

Example: The AI outputs:

import logging
import time
from django.utils.deprecation import MiddlewareMixin

logger = logging.getLogger('django.request')

class RequestLoggingMiddleware(MiddlewareMixin):
    def process_request(self, request):
        if 'admin' not in request.path and 'static' not in request.path:
            request._start_time = time.time()

    def process_response(self, request, response):
        if hasattr(request, '_start_time'):
            duration = time.time() - request._start_time
            user = request.user if request.user.is_authenticated else 'anonymous'
            logger.info(f'{request.method} {request.path} - {response.status_code} - {user} - {duration:.2f}s')
        return response

Tip: Add 'django.request': {'handlers': ['file'], 'level': 'INFO'} to LOGGING in settings.py. Ask the AI to include that config snippet.

8. Form Validation with Clean Methods

Prompt: "Create a Django ModelForm for BlogPost that validates: title must be unique (case-insensitive), content must be at least 50 characters, and status can only be 'draft' if the user is not a superuser. Use clean_field methods and a custom validator for content length."

Example: The AI generates:

from django import forms
from .models import BlogPost

class BlogPostForm(forms.ModelForm):
    class Meta:
        model = BlogPost
        fields = ['title', 'content', 'status']

    def clean_title(self):
        title = self.cleaned_data['title']
        if BlogPost.objects.filter(title__iexact=title).exists():
            raise forms.ValidationError('Title already exists (case-insensitive).')
        return title

    def clean_content(self):
        content = self.cleaned_data['content']
        if len(content) < 50:
            raise forms.ValidationError('Content must be at least 50 characters.')
        return content

    def clean_status(self):
        status = self.cleaned_data['status']
        user = self.initial.get('user')
        if status == 'draft' and user and not user.is_superuser:
            raise forms.ValidationError('Only superusers can set status to draft.')
        return status

Tip: Pass the user in form's __init__ or via initial. The AI often forgets to handle the user context—remind it.

9. Management Command for Periodic Tasks

Prompt: "Write a Django management command that archives all BlogPosts older than 30 days with status 'published'. The command should accept a --dry-run flag that only prints what would be archived, without making changes. Also log the number of archived posts."

Example: The AI creates a command:

from django.core.management.base import BaseCommand
from django.utils import timezone
from blog.models import BlogPost
import logging

logger = logging.getLogger(__name__)

class Command(BaseCommand):
    help = 'Archive published posts older than 30 days'

    def add_arguments(self, parser):
        parser.add_argument('--dry-run', action='store_true', help='Only show posts that would be archived')

    def handle(self, *args, **options):
        cutoff = timezone.now() - timezone.timedelta(days=30)
        old_posts = BlogPost.objects.filter(status='published', published_date__lt=cutoff)
        count = old_posts.count()
        if options['dry_run']:
            self.stdout.write(f'Would archive {count} posts:')
            for post in old_posts:
                self.stdout.write(f'  - {post.title}')
        else:
            updated = old_posts.update(status='archived')
            logger.info(f'Archived {updated} posts.')
            self.stdout.write(self.style.SUCCESS(f'Archived {updated} posts.'))

Tip: Dry-run is a must for destructive operations. Schedule this command with cron or Celery Beat.

10. Testing with Factory Boy and Pytest

Prompt: "Write a pytest test suite for the BlogPost model and API. Use Factory Boy to create test data. Test: model string representation, custom manager method published(), API list endpoint returns only published posts, and API create endpoint returns 403 for anonymous users. Use Django's test client and APIClient from DRF."

Example: The AI generates:

import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from .models import BlogPost
from .factories import BlogPostFactory

@pytest.fixture
def api_client():
    return APIClient()

@pytest.mark.django_db
def test_model_str():
    post = BlogPostFactory(title='Test')
    assert str(post) == 'Test'

@pytest.mark.django_db
def test_published_manager():
    BlogPostFactory(status='draft')
    BlogPostFactory(status='published')
    assert BlogPost.published().count() == 1

@pytest.mark.django_db
def test_api_list_only_published(api_client):
    BlogPostFactory(status='draft')
    BlogPostFactory(status='published')
    url = reverse('blogpost-list')
    response = api_client.get(url)
    assert response.status_code == status.HTTP_200_OK
    assert len(response.data) == 1

@pytest.mark.django_db
def test_api_create_requires_auth(api_client):
    url = reverse('blogpost-list')
    data = {'title': 'New', 'content': 'x'*100, 'status': 'published'}
    response = api_client.post(url, data, format='json')
    assert response.status_code == status.HTTP_403_FORBIDDEN

Tip: Ask the AI to include a conftest.py fixture for the API client and to use @pytest.mark.django_db on every test.

Conclusion

These 10 prompts are my daily toolkit for Django development. They cover the full stack—from models and forms to views, API, admin, signals, and tests. The key is specificity: vague prompts return vague code. Always specify field types, constraints, permission classes, and edge cases. Copy these into your AI chat, adjust the model names, and watch your productivity soar. Start with prompt #1 today—your future self will thank you.

Try one of these prompts in your next Django sprint. Share your most effective prompt in the comments below!

← All posts

Comments