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

FeatureDRFDjango Ninja
Type hintsOptionalRequired
ValidationSerializersPydantic
Async supportPartialFull
Auto docsdrf-spectacularBuilt-in
Learning curveMediumEasy
Raw performanceGoodBetter

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

Keep DRF When

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.

All posts