From python-experts
Guides Django REST API development with Ninja (async, Pydantic) or DRF. Covers framework choice, setup, schemas, endpoints, auth, permissions, filtering, pagination, async.
How this skill is triggered — by the user, by Claude, or both
Slash command
/python-experts:django-apiThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
| Factor | Django Ninja | Django REST Framework |
| Factor | Django Ninja | Django REST Framework |
|---|---|---|
| Best for | New projects, performance-critical, type-safety | Complex apps, mature ecosystem needs |
| Validation | Pydantic (type hints) | Serializers |
| Async | Native, first-class | Via adrf package |
| Docs | Auto-generated OpenAPI | Via drf-spectacular |
| Learning curve | Lower (FastAPI-like) | Steeper but well-documented |
| Ecosystem | Growing | Extensive third-party packages |
Recommendation: Start with Django Ninja for new projects. Use DRF when you need its ecosystem (complex permissions, nested routers, etc).
pip install django-ninja
# config/urls.py
from ninja import NinjaAPI
api = NinjaAPI(
title="My API",
version="1.0.0",
docs_url="/docs", # Swagger UI at /api/docs
)
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", api.urls),
]
# apps/blog/api/schemas.py
from ninja import Schema, ModelSchema
from datetime import datetime
from apps.blog.models import Article
class ArticleIn(Schema):
title: str
body: str
tag_ids: list[int] = []
class ArticleOut(ModelSchema):
author_name: str
class Meta:
model = Article
fields = ["id", "title", "slug", "status", "created_at"]
@staticmethod
def resolve_author_name(obj: Article) -> str:
return obj.author.username
class ArticleDetailOut(ArticleOut):
body: str
tags: list[str]
@staticmethod
def resolve_tags(obj: Article) -> list[str]:
return [t.name for t in obj.tags.all()]
Key patterns:
Schema for input, ModelSchema for outputresolve_* for computed fields# apps/blog/api/views.py
from ninja import Router
from django.shortcuts import get_object_or_404
from apps.blog.models import Article
from .schemas import ArticleIn, ArticleOut, ArticleDetailOut
router = Router(tags=["articles"])
@router.get("/", response=list[ArticleOut])
def list_articles(request, status: str | None = None):
qs = Article.objects.select_related("author")
if status:
qs = qs.filter(status=status)
return qs
@router.get("/{slug}", response=ArticleDetailOut)
def get_article(request, slug: str):
return get_object_or_404(
Article.objects.select_related("author").prefetch_related("tags"),
slug=slug,
)
@router.post("/", response=ArticleOut)
def create_article(request, payload: ArticleIn):
article = Article.objects.create(
author=request.user,
title=payload.title,
body=payload.body,
)
if payload.tag_ids:
article.tags.set(payload.tag_ids)
return article
# apps/blog/api/views.py
from ninja import Router
from asgiref.sync import sync_to_async
router = Router()
@router.get("/articles/", response=list[ArticleOut])
async def list_articles(request):
# Wrap ORM calls with sync_to_async
qs = await sync_to_async(list)(
Article.objects.select_related("author")[:20]
)
return qs
@router.get("/external-data/")
async def fetch_external(request):
import httpx
async with httpx.AsyncClient() as client:
resp = await client.get("https://api.example.com/data")
return resp.json()
When to use async:
Note: Django ORM is not fully async — wrap with sync_to_async.
# apps/core/api/auth.py
from ninja.security import HttpBearer, APIKeyHeader
from django.contrib.auth.models import User
class AuthBearer(HttpBearer):
def authenticate(self, request, token: str) -> User | None:
# Validate JWT or token
try:
return User.objects.get(auth_token=token)
except User.DoesNotExist:
return None
class ApiKey(APIKeyHeader):
param_name = "X-API-Key"
def authenticate(self, request, key: str) -> User | None:
try:
return User.objects.get(api_key=key)
except User.DoesNotExist:
return None
# Usage
@router.get("/protected/", auth=AuthBearer())
def protected_endpoint(request):
return {"user": request.auth.username}
# config/urls.py
from ninja import NinjaAPI
from apps.blog.api.views import router as blog_router
from apps.users.api.views import router as users_router
api = NinjaAPI()
api.add_router("/articles", blog_router)
api.add_router("/users", users_router)
urlpatterns = [
path("api/v1/", api.urls),
]
Use when you need the mature ecosystem or complex features.
# config/settings/base.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
# apps/blog/api/serializers.py
from rest_framework import serializers
from apps.blog.models import Article
class ArticleListSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField()
class Meta:
model = Article
fields = ["id", "title", "slug", "author", "status", "created_at"]
class ArticleDetailSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField(read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
queryset=Tag.objects.all(), many=True, write_only=True, source="tags"
)
class Meta:
model = Article
fields = ["id", "title", "slug", "body", "author", "tags", "tag_ids", "created_at"]
read_only_fields = ["slug", "created_at"]
# apps/blog/api/views.py
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.select_related("author").prefetch_related("tags")
lookup_field = "slug"
def get_serializer_class(self):
if self.action == "list":
return ArticleListSerializer
return ArticleDetailSerializer
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=True, methods=["post"])
def publish(self, request, slug=None):
article = self.get_object()
article.status = "published"
article.save(update_fields=["status"])
return Response({"status": "published"})
pip install adrf
from adrf.viewsets import ViewSet
from rest_framework.response import Response
class AsyncArticleViewSet(ViewSet):
async def list(self, request):
articles = await sync_to_async(list)(Article.objects.all()[:20])
serializer = ArticleListSerializer(articles, many=True)
return Response(serializer.data)
# Django Ninja
@router.get("/", response=list[ArticleOut])
def list_articles(
request,
status: str | None = None,
author: str | None = None,
created_after: date | None = None,
):
qs = Article.objects.all()
if status:
qs = qs.filter(status=status)
if author:
qs = qs.filter(author__username=author)
if created_after:
qs = qs.filter(created_at__date__gte=created_after)
return qs
# DRF with django-filter
class ArticleFilter(django_filters.FilterSet):
created_after = django_filters.DateFilter(field_name="created_at", lookup_expr="gte")
class Meta:
model = Article
fields = ["status", "author__username"]
# Django Ninja
from ninja.pagination import paginate, PageNumberPagination
@router.get("/", response=list[ArticleOut])
@paginate(PageNumberPagination, page_size=20)
def list_articles(request):
return Article.objects.all()
# Django Ninja
from ninja.errors import HttpError
@router.get("/{id}")
def get_article(request, id: int):
try:
return Article.objects.get(id=id)
except Article.DoesNotExist:
raise HttpError(404, "Article not found")
# Django Ninja
from ninja.testing import TestClient
from config.urls import api
client = TestClient(api)
def test_list_articles():
response = client.get("/articles/")
assert response.status_code == 200
def test_create_article(authenticated_client):
response = authenticated_client.post(
"/articles/",
json={"title": "Test", "body": "Content"},
)
assert response.status_code == 200
For async support, use an ASGI server:
# Development
uvicorn config.asgi:application --reload
# Production
gunicorn config.asgi:application -k uvicorn.workers.UvicornWorker -w 4
Mixing sync/async incorrectly: Use sync_to_async for ORM in async views. Don't call sync code directly.
N+1 queries: Always select_related/prefetch_related — both frameworks need this.
Blocking in async views: Use httpx (async) not requests (sync) for external calls.
Over-engineering auth: Start simple. Django Ninja's built-in HttpBearer or DRF's IsAuthenticated cover most cases.
No OpenAPI docs: Django Ninja auto-generates. For DRF, always add drf-spectacular.
npx claudepluginhub jpoutrin/product-forge --plugin python-expertsProvides Django Ninja patterns for API development with one-endpoint-per-file organization, domain-grouped routers, separate Pydantic schemas, and services for business logic.
Guides Django and FastAPI development: project structure, DRF serializers/viewsets, Pydantic, async ASGI support, admin customization, ORM optimization, deployment.
Provides 2025 Django patterns for project structure, settings, naming conventions, models with type hints and indexes, async support. Activates on Django models, views, URLs, forms, templates, commands, structure.