Awesome-omni-skill 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.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/django-api-majiayu000" ~/.claude/skills/diegosouzapw-awesome-omni-skill-django-api-46353c && rm -rf "$T"
skills/development/django-api-majiayu000/SKILL.mdDjango 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 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
for input,Schema
for outputModelSchema - Type hints drive validation and docs
- Use
for computed fieldsresolve_*
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
sync_to_async.
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
-
Mixing sync/async incorrectly: Use
for ORM in async views. Don't call sync code directly.sync_to_async -
N+1 queries: Always
/select_related
— both frameworks need this.prefetch_related -
Blocking in async views: Use
(async) nothttpx
(sync) for external calls.requests -
Over-engineering auth: Start simple. Django Ninja's built-in
or DRF'sHttpBearer
cover most cases.IsAuthenticated -
No OpenAPI docs: Django Ninja auto-generates. For DRF, always add drf-spectacular.