smithery/jpoutrin

django-api

Django API development for 2025. Covers Django Ninja (modern, async-first, type-safe) and Django REST Framework (mature, ecosystem-rich). Use when building REST APIs, choosing between frameworks, implementing authentication, permissions, filtering, pagination, or async endpoints.

Installation

$ npx skills add smithery/jpoutrin --skill django-api

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/jpoutrin · top by installs.

npx skills add smithery/jpoutrin

Browse all from smithery/jpoutrin

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 10,280 B
  • docs SUMMARY.md 298 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

Django API Development (2025)

Framework Choice

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).


Django Ninja (Recommended for 2025)

Setup

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),
]

Schemas (Pydantic)

# 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:

  • Use Schema for input, ModelSchema for output
  • Type hints drive validation and docs
  • Use resolve_* for computed fields

Endpoints

# 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

Async Endpoints

# 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:

  • External API calls (httpx, aiohttp)
  • Multiple concurrent I/O operations
  • Real-time / high-concurrency endpoints

Note: Django ORM is not fully async — wrap with synctoasync.

Authentication

# 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}

Wiring Routers

# 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),
]

Django REST Framework

Use when you need the mature ecosystem or complex features.

Setup

# 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",
}

Serializers

# 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"]

ViewSets

# 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"})

Async DRF (via adrf)

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)

Common Patterns (Both Frameworks)

Filtering

# 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"]

Pagination

# 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()

Error Handling

# 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")

Testing

# 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

Running with ASGI

For async support, use an ASGI server:

# Development
uvicorn config.asgi:application --reload

# Production
gunicorn config.asgi:application -k uvicorn.workers.UvicornWorker -w 4

Common Pitfalls

  1. Mixing sync/async incorrectly: Use synctoasync for ORM in async views. Don't call sync code directly.
  1. N+1 queries: Always selectrelated/prefetchrelated — both frameworks need this.
  1. Blocking in async views: Use httpx (async) not requests (sync) for external calls.
  1. Over-engineering auth: Start simple. Django Ninja's built-in HttpBearer or DRF's IsAuthenticated cover most cases.
  1. No OpenAPI docs: Django Ninja auto-generates. For DRF, always add drf-spectacular.