Building APIs with Django Ninja
django python
Django REST Framework (DRF) has been the standard for years. But Django Ninja offers a compelling alternative: faster, type-safe, and inspired by FastAPI. Here’s the rundown.
Why Django Ninja
| Feature | DRF | Django Ninja |
|---|---|---|
| Type hints | Optional | Required |
| Validation | Serializers | Pydantic |
| Async support | Partial | Full |
| Auto docs | drf-spectacular | Built-in |
| Learning curve | Medium | Easy |
| Raw performance | Good | Better |
Installation
pip install django-ninja
Hello World
# api.py
from ninja import NinjaAPI
api = NinjaAPI()
@api.get("/hello")
def hello(request, name: str = "World"):
return {"message": f"Hello, {name}!"}
# urls.py
from django.urls import path
from .api import api
urlpatterns = [
path("api/", api.urls),
]
Visit /api/docs for automatic OpenAPI documentation.
Pydantic Schemas
Basic Schema
from ninja import Schema
from datetime import datetime
class ArticleIn(Schema):
title: str
content: str
published: bool = False
class ArticleOut(Schema):
id: int
title: str
content: str
published: bool
created_at: datetime
Using with Views
from ninja import Router
from .models import Article
from .schemas import ArticleIn, ArticleOut
router = Router()
@router.post("/articles", response=ArticleOut)
def create_article(request, payload: ArticleIn):
article = Article.objects.create(**payload.dict())
return article
@router.get("/articles", response=list[ArticleOut])
def list_articles(request):
return Article.objects.all()
CRUD Operations
from django.shortcuts import get_object_or_404
from ninja import Router
from .models import Article
from .schemas import ArticleIn, ArticleOut
router = Router()
# Create
@router.post("/articles", response=ArticleOut)
def create_article(request, payload: ArticleIn):
article = Article.objects.create(**payload.dict())
return article
# Read (list)
@router.get("/articles", response=list[ArticleOut])
def list_articles(request):
return Article.objects.all()
# Read (detail)
@router.get("/articles/{article_id}", response=ArticleOut)
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id)
# Update
@router.put("/articles/{article_id}", response=ArticleOut)
def update_article(request, article_id: int, payload: ArticleIn):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.dict().items():
setattr(article, attr, value)
article.save()
return article
# Delete
@router.delete("/articles/{article_id}")
def delete_article(request, article_id: int):
article = get_object_or_404(Article, id=article_id)
article.delete()
return {"success": True}
Authentication
Django Session Auth
from ninja.security import django_auth
api = NinjaAPI(auth=django_auth)
@api.get("/private")
def private_endpoint(request):
return {"user": request.user.username}
API Key Auth
from ninja.security import APIKeyHeader
class ApiKey(APIKeyHeader):
param_name = "X-API-Key"
def authenticate(self, request, key):
if key == "secret-key":
return key
return None
api_key = ApiKey()
@api.get("/private", auth=api_key)
def private_endpoint(request):
return {"authenticated": True}
JWT Auth
from ninja.security import HttpBearer
import jwt
class JWTAuth(HttpBearer):
def authenticate(self, request, token):
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
user_id = payload.get("user_id")
return User.objects.get(id=user_id)
except:
return None
jwt_auth = JWTAuth()
@api.get("/me", auth=jwt_auth)
def get_me(request):
return {"user": request.auth.username}
Query Parameters and Filtering
from ninja import Query
from typing import Optional
class ArticleFilter(Schema):
search: Optional[str] = None
published: Optional[bool] = None
category: Optional[str] = None
@router.get("/articles", response=list[ArticleOut])
def list_articles(request, filters: ArticleFilter = Query(...)):
qs = Article.objects.all()
if filters.search:
qs = qs.filter(title__icontains=filters.search)
if filters.published is not None:
qs = qs.filter(published=filters.published)
if filters.category:
qs = qs.filter(category=filters.category)
return qs
File Uploads
from ninja import File, UploadedFile
@router.post("/upload")
def upload_file(request, file: UploadedFile = File(...)):
with open(f"uploads/{file.name}", "wb") as f:
for chunk in file.chunks():
f.write(chunk)
return {"filename": file.name, "size": file.size}
Error Handling
from ninja.errors import HttpError
@router.get("/articles/{article_id}")
def get_article(request, article_id: int):
try:
return Article.objects.get(id=article_id)
except Article.DoesNotExist:
raise HttpError(404, "Article not found")
# Custom exception handler
@api.exception_handler(ValidationError)
def validation_error_handler(request, exc):
return api.create_response(
request,
{"message": "Validation failed", "details": exc.errors()},
status=422,
)
Async Support
import httpx
@router.get("/external-data")
async def get_external_data(request):
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/data")
return response.json()
@router.get("/db-async")
async def get_data_async(request):
articles = await Article.objects.all().afirst()
return {"article": articles.title if articles else None}
Router Organization
# api/__init__.py
from ninja import NinjaAPI
from .articles import router as articles_router
from .users import router as users_router
api = NinjaAPI()
api.add_router("/articles", articles_router, tags=["articles"])
api.add_router("/users", users_router, tags=["users"])
# api/articles.py
from ninja import Router
router = Router()
@router.get("/")
def list_articles(request):
...
Comparison with DRF
DRF Approach
from rest_framework import serializers, viewsets
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
Django Ninja Approach
from ninja import Router, Schema
class ArticleSchema(Schema):
id: int
title: str
router = Router()
@router.get("/", response=list[ArticleSchema])
def list_articles(request):
return Article.objects.all()
Django Ninja is more explicit, type-safe, and often less code.
When to Use
Choose Django Ninja When
- Starting a new Django API project
- You want FastAPI-like syntax in Django
- Type safety is a priority
- You need async endpoints
- Performance matters
Keep DRF When
- Existing DRF project
- Need complex serializer features
- Team knows DRF well
- Third-party DRF extensions required
Final Thoughts
Django Ninja brings modern Python API patterns to Django. Type hints, Pydantic validation, auto-docs—it’s what FastAPI did for Starlette, but for Django.
For new APIs, it’s worth considering.
Modern APIs, Django foundation.