Une API Django qui fonctionne pour 100 utilisateurs ne fonctionnera pas forcément pour 10 000. Ces patterns, je les ai implémentés en réaction à des incidents en production. Autant les mettre en place dès le début.


Pagination : ne jamais renvoyer toute la collection


La pagination par offset (?page=5) est simple mais dangereuse sur les grands datasets : une requête OFFSET 50000 scanne les 50 000 premières lignes avant de renvoyer les 20 suivantes. La pagination par curseur est supérieure :


class ArticleCursorPagination(CursorPagination):
    page_size = 20
    ordering = "-created_at"


Performance constante quelle que soit la page, résistante aux insertions en temps réel.


Index BDD : le levier le plus sous-estimé


class Article(models.Model):
    class Meta:
        indexes = [
            # Requêtes filtrées par statut + triées par date
            models.Index(fields=["statut", "-date_publication"]),
            models.Index(fields=["titre"], name="idx_article_titre"),
        ]


Utilisez EXPLAIN ANALYZE en PostgreSQL ou EXPLAIN en MySQL pour détecter les seq scans. Un index bien placé peut transformer une requête de 2s en 5ms.


Select related : éliminer les N+1 en amont


# ? N+1 : 1 requête pour la liste + 1 par article pour le thème
articles = Article.objects.filter(statut="publie")

# ? 2 requêtes pour tout
articles = (
    Article.objects
    .filter(statut="publie")
    .select_related("theme")
    .prefetch_related("tags", "commentaires")
)


Installez django-debug-toolbar pour détecter les N+1 automatiquement en développement.


Throttling : protéger l'API dès le départ


REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "100/day",
        "user": "1000/day",
    },
}


Cache Redis : mesurer avant d'optimiser


from django.core.cache import cache

def get_articles_vedette():
    key = "articles:vedette:v1"
    articles = cache.get(key)
    if articles is None:
        articles = list(
            Article.objects.filter(est_en_vedette=True, statut="publie")
            .select_related("theme").prefetch_related("tags")[:6]
        )
        cache.set(key, articles, timeout=1800)
    return articles


Monitoring : ce qu'il faut mesurer en production


  • Latence P50, P95, P99 par endpoint (pas juste la moyenne)
  • Taux d'erreur 4xx / 5xx — un pic de 4xx peut indiquer une attaque
  • Hits/misses de cache — un taux de miss > 80% signale un problème
  • Slow queries — PostgreSQL a log_min_duration_statement

?

Stack recommandée : Sentry (erreurs) + Prometheus/Grafana (métriques).