💻 Développement

dev-django-guide

Développement d'applications Python Django avec models, views, templates, ORM, admin et Django REST Framework.

⚡ Installation & lancement en 1 commande

Copiez-collez dans votre terminal : le skill s'installe dans ~/.claude/skills et Claude Code se lance directement dessus.

macOS / Linux
curl -fsSL https://raw.githubusercontent.com/khalilbenaz/claude-skills-collection/main/install.sh | sh -s -- dev-django-guide --launch
Windows (PowerShell)
iex "& { $(iwr -useb https://raw.githubusercontent.com/khalilbenaz/claude-skills-collection/main/install.ps1) } dev-django-guide -Launch"

🚀 Déjà installé ?

claude "/dev-django-guide"

Ou tapez /dev-django-guide dans une session Claude Code, ou décrivez simplement votre besoin — le skill se déclenche automatiquement via le skill-router.

🔑 Déclencheurs automatiques

Le skill s'active automatiquement quand votre demande contient :

DjangoDjango RESTDRFORM Djangomodel Djangovue Django

📦 Installation manuelle

git clone https://github.com/khalilbenaz/claude-skills-collection.git cp -r claude-skills-collection/skills/dev-django-guide ~/.claude/skills/

Payload du plugin : skills/dev-django-guide · source éditable : dev-skills/django-guide

📖 Manuel

Guide Django

1. Choisir l'architecture

BesoinArchitecture recommandée
API consommée par SPA/mobileDjango + DRF uniquement (rest_framework)
Back-office interneDjango + admin personnalisé
Application web rendue côté serveurDjango + templates + HTMX
Hybride API + pages publiquesDjango + DRF + templates pour les pages non-auth

2. Initialiser le projet

pip install django djangorestframework django-environ
django-admin startproject config .   # config = dossier settings
python manage.py startapp orders     # une app par domaine métier

Structure recommandée :

project/
  config/
    settings/
      base.py      # commun
      dev.py       # DEBUG=True, SQLite
      prod.py      # ALLOWED_HOSTS, DATABASES Postgres, STATIC_ROOT
    urls.py
    wsgi.py
  orders/
    models.py
    views.py
    serializers.py
    urls.py
    admin.py
    tests/
  manage.py
  .env

config/settings/base.py :

from environ import Env
env = Env()
Env.read_env()

SECRET_KEY = env("SECRET_KEY")
DATABASES = {"default": env.db()}  # DATABASE_URL=postgres://...

3. Concevoir les models

from django.db import models
from django.utils.translation import gettext_lazy as _

class Order(models.Model):
    class Status(models.TextChoices):
        PENDING  = "pending",  _("En attente")
        SHIPPED  = "shipped",  _("Expédié")

    reference   = models.CharField(max_length=32, unique=True)
    customer    = models.ForeignKey("users.User", on_delete=models.PROTECT,
                                    related_name="orders")
    status      = models.CharField(max_length=16, choices=Status.choices,
                                    default=Status.PENDING)
    created_at  = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]
        indexes  = [models.Index(fields=["status", "created_at"])]

    def __str__(self):
        return f"Order {self.reference}"

    def clean(self):
        # Validation métier centralisée ici, pas dans la vue
        if self.status == self.Status.SHIPPED and not self.reference:
            raise ValidationError("Reference requise avant expédition.")

Migrations :

python manage.py makemigrations orders
python manage.py migrate
# Data migration
python manage.py makemigrations orders --empty --name backfill_reference

Data migration réversible :

def forwards(apps, schema_editor):
    Order = apps.get_model("orders", "Order")
    Order.objects.filter(reference="").update(reference="LEGACY")

def backwards(apps, schema_editor):
    pass  # irréversible acceptable si documenté

class Migration(migrations.Migration):
    operations = [migrations.RunPython(forwards, backwards)]

4. ORM — éviter le N+1

# MAL — N+1
orders = Order.objects.all()
for o in orders:
    print(o.customer.email)  # 1 requête par ligne

# BIEN
orders = Order.objects.select_related("customer").all()

# Relations M2M
orders = Order.objects.prefetch_related("items__product").all()

# Annoter en base plutôt que Python
from django.db.models import Count, Sum
Order.objects.annotate(total=Sum("items__price")).filter(total__gt=100)

5. DRF — sérializers et ViewSets

# serializers.py
class OrderSerializer(serializers.ModelSerializer):
    customer_email = serializers.EmailField(source="customer.email", read_only=True)

    class Meta:
        model  = Order
        fields = ["id", "reference", "status", "customer_email", "created_at"]
        read_only_fields = ["created_at"]

    def validate_status(self, value):
        allowed = ["pending", "shipped"]
        if value not in allowed:
            raise serializers.ValidationError(f"Statut invalide. Valeurs : {allowed}")
        return value

# views.py
from rest_framework import viewsets, permissions, filters
from django_filters.rest_framework import DjangoFilterBackend

class OrderViewSet(viewsets.ModelViewSet):
    serializer_class   = OrderSerializer
    permission_classes = [permissions.IsAuthenticated]
    filter_backends    = [DjangoFilterBackend, filters.OrderingFilter]
    filterset_fields   = ["status"]
    ordering_fields    = ["created_at"]

    def get_queryset(self):
        return Order.objects.select_related("customer")\
                            .filter(customer=self.request.user)

    def perform_create(self, serializer):
        serializer.save(customer=self.request.user)
# urls.py (app)
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("orders", OrderViewSet, basename="order")
urlpatterns = router.urls

6. Admin utile

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display  = ["reference", "customer", "status", "created_at"]
    list_filter   = ["status", "created_at"]
    search_fields = ["reference", "customer__email"]
    raw_id_fields = ["customer"]     # évite le <select> avec milliers d'entrées
    actions       = ["mark_shipped"]

    @admin.action(description="Marquer comme expédié")
    def mark_shipped(self, request, queryset):
        updated = queryset.update(status=Order.Status.SHIPPED)
        self.message_user(request, f"{updated} commande(s) expédiée(s).")

7. Authentification JWT (DRF)

pip install djangorestframework-simplejwt
# settings/base.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
}

SIMPLE_JWT = {"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
              "REFRESH_TOKEN_LIFETIME": timedelta(days=7)}
# urls.py racine
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
path("api/token/",         TokenObtainPairView.as_view()),
path("api/token/refresh/", TokenRefreshView.as_view()),

8. Tests

from rest_framework.test import APITestCase
from rest_framework import status

class OrderAPITest(APITestCase):
    def setUp(self):
        self.user  = UserFactory()
        self.order = OrderFactory(customer=self.user)
        self.client.force_authenticate(user=self.user)

    def test_list_returns_own_orders_only(self):
        OrderFactory()  # commande d'un autre user
        r = self.client.get("/api/orders/")
        self.assertEqual(r.status_code, status.HTTP_200_OK)
        self.assertEqual(len(r.data["results"]), 1)
pip install factory-boy pytest-django
pytest --reuse-db   # réutilise la DB entre runs pour la vitesse

9. Déploiement production

pip install gunicorn whitenoise
# settings/prod.py
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
python manage.py collectstatic --no-input
gunicorn config.wsgi:application --workers 4 --bind 0.0.0.0:8000

Variables d'environnement obligatoires en prod :

SECRET_KEY=...
DATABASE_URL=postgres://user:pass@host:5432/db
DJANGO_SETTINGS_MODULE=config.settings.prod
ALLOWED_HOSTS=mondomaine.com

Garde-fous / anti-patterns

Anti-patternProblèmeCorrection
Logique métier dans la vueCouplage, non-testableService layer ou méthode model
objects.all() sans filtre en vue listeFull table scanToujours filtrer + paginer
get_or_create sans defaults=Race conditionUtiliser update_or_create ou transaction atomique
Migration avec RunPython non réversible non documentéeRollback impossibleAjouter reverse_code ou commenter l'intention
DEBUG=True en prodExposition des stacktracesVariable d'env + vérification au démarrage
Secrets dans settings.pyFuite via repodjango-environ ou vault
null=True sur CharFieldDeux valeurs "vide" ("" et None)blank=True uniquement sauf FK
Serializer depth=2 en API publiqueSur-exposition de données imbriquéesChamps explicites + read_only

Bonnes pratiques 2026