By KFSys
System Administrator
When building APIs with Django, developers typically face a choice between two powerful frameworks: Django REST Framework (DRF) and Django Ninja. Both serve the same fundamental purpose—creating robust REST APIs—but take dramatically different approaches to achieve this goal.
This tutorial provides an in-depth technical comparison to help you choose the right tool for your project.
Django REST Framework (DRF)
Django Ninja
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
DRF follows Django’s philosophy closely, extending Django’s patterns into the API realm:
# DRF ViewSet approach
from rest_framework import viewsets, serializers
from rest_framework.response import Response
from .models import Book
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = '__all__'
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
def create(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
book = serializer.save()
return Response(serializer.data, status=201)
Ninja embraces modern Python features and prioritizes developer experience:
# Django Ninja approach
from ninja import NinjaAPI, Schema
from typing import List
from .models import Book
api = NinjaAPI()
class BookSchema(Schema):
title: str
author: str
isbn: str
published_date: date
class BookOut(Schema):
id: int
title: str
author: str
isbn: str
@api.post("/books", response=BookOut)
def create_book(request, book: BookSchema):
book_instance = Book.objects.create(**book.dict())
return book_instance
Django Ninja generally outperforms DRF in raw throughput:
# Performance test setup
import time
from django.test import TestCase, Client
class PerformanceTest(TestCase):
def setUp(self):
self.client = Client()
# Create test data
for i in range(1000):
Book.objects.create(
title=f"Book {i}",
author=f"Author {i}",
isbn=f"978-{i:010d}"
)
def test_list_performance(self):
# DRF endpoint
start = time.time()
response = self.client.get('/api/drf/books/')
drf_time = time.time() - start
# Ninja endpoint
start = time.time()
response = self.client.get('/api/ninja/books/')
ninja_time = time.time() - start
print(f"DRF: {drf_time:.4f}s, Ninja: {ninja_time:.4f}s")
Typical Results:
# Memory profiling example
import tracemalloc
tracemalloc.start()
# Your API calls here
response = client.get('/api/books/')
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.2f} MB")
DRF Validation:
class BookSerializer(serializers.ModelSerializer):
title = serializers.CharField(max_length=200)
isbn = serializers.CharField(validators=[isbn_validator])
class Meta:
model = Book
fields = ['title', 'author', 'isbn', 'published_date']
def validate_isbn(self, value):
if not value.startswith('978'):
raise serializers.ValidationError("ISBN must start with 978")
return value
def validate(self, data):
if data['published_date'] > timezone.now().date():
raise serializers.ValidationError("Publication date cannot be in the future")
return data
Ninja Schema Validation:
from pydantic import validator
from datetime import date
class BookSchema(Schema):
title: str = Field(..., max_length=200)
author: str
isbn: str
published_date: date
@validator('isbn')
def validate_isbn(cls, v):
if not v.startswith('978'):
raise ValueError('ISBN must start with 978')
return v
@validator('published_date')
def validate_publication_date(cls, v):
if v > date.today():
raise ValueError('Publication date cannot be in the future')
return v
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import action
class BookViewSet(viewsets.ModelViewSet):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
@action(detail=True, methods=['post'])
def set_favorite(self, request, pk=None):
book = self.get_object()
# Custom logic here
return Response({'status': 'favorite set'})
from ninja.security import HttpBearer
from django.contrib.auth import authenticate
class AuthBearer(HttpBearer):
def authenticate(self, request, token):
try:
user = Token.objects.get(key=token).user
return user
except Token.DoesNotExist:
return None
@api.post("/books", auth=AuthBearer())
def create_book(request, book: BookSchema):
# request.auth contains the authenticated user
return Book.objects.create(**book.dict())
DRF with django-filter:
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
class BookViewSet(viewsets.ModelViewSet):
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['author', 'published_date']
search_fields = ['title', 'author']
def get_queryset(self):
queryset = Book.objects.all()
author = self.request.query_params.get('author')
if author:
queryset = queryset.filter(author__icontains=author)
return queryset
Ninja Filtering:
from ninja import Query
@api.get("/books")
def list_books(request,
author: str = Query(None),
search: str = Query(None),
limit: int = Query(10)):
books = Book.objects.all()
if author:
books = books.filter(author__icontains=author)
if search:
books = books.filter(title__icontains=search)
return books[:limit]
DRF Pagination:
from rest_framework.pagination import PageNumberPagination
class BookPagination(PageNumberPagination):
page_size = 20
page_size_query_param = 'page_size'
max_page_size = 100
class BookViewSet(viewsets.ModelViewSet):
pagination_class = BookPagination
Ninja Pagination:
@api.get("/books")
def list_books(request,
page: int = Query(1),
page_size: int = Query(20)):
offset = (page - 1) * page_size
books = Book.objects.all()[offset:offset + page_size]
total = Book.objects.count()
return {
"items": books,
"total": total,
"page": page,
"pages": (total + page_size - 1) // page_size
}
from rest_framework.test import APITestCase
from rest_framework import status
class BookAPITest(APITestCase):
def setUp(self):
self.user = User.objects.create_user('testuser', 'test@test.com', 'pass')
self.client.force_authenticate(user=self.user)
def test_create_book(self):
data = {
'title': 'Test Book',
'author': 'Test Author',
'isbn': '9781234567890'
}
response = self.client.post('/api/books/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Book.objects.count(), 1)
from django.test import TestCase, Client
class BookNinjaTest(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('testuser', 'test@test.com', 'pass')
def test_create_book(self):
self.client.force_login(self.user)
data = {
'title': 'Test Book',
'author': 'Test Author',
'isbn': '9781234567890'
}
response = self.client.post('/api/books/',
data=json.dumps(data),
content_type='application/json')
self.assertEqual(response.status_code, 201)
django-oauth-toolkit
for OAuth2django-rest-swagger
for documentationdjango-filter
for advanced filteringdrf-spectacular
for OpenAPI 3.0# Example: Complex enterprise API with custom permissions
class AuthorPermission(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
return obj.author == request.user
class BookViewSet(viewsets.ModelViewSet):
permission_classes = [AuthorPermission]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = BookFilter
search_fields = ['title', 'author', 'description']
ordering_fields = ['created_at', 'title']
# Example: Modern API with type safety
from typing import Optional
from ninja import NinjaAPI, Schema
from ninja.pagination import paginate
api = NinjaAPI(title="Book API", version="1.0.0")
@api.get("/books", response=List[BookOut])
@paginate
def list_books(request,
author: Optional[str] = None,
genre: Optional[str] = None):
books = Book.objects.all()
if author:
books = books.filter(author__icontains=author)
if genre:
books = books.filter(genre=genre)
return books
# Step 1: Gradual migration approach
# Keep existing DRF endpoints, add new ones with Ninja
# urls.py
urlpatterns = [
path('api/v1/', include('myapp.drf_urls')), # Existing DRF
path('api/v2/', api.urls), # New Ninja API
]
# Step 2: Convert serializers to schemas
# DRF Serializer
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = '__all__'
# Equivalent Ninja Schema
class BookSchema(ModelSchema):
class Config:
model = Book
model_fields = '__all__'
# 1. Use ViewSets for consistent CRUD operations
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.select_related('author')
serializer_class = BookSerializer
def get_queryset(self):
# Always optimize queries
return super().get_queryset().select_related('author')
# 2. Custom serializer methods for complex logic
class BookSerializer(serializers.ModelSerializer):
author_name = serializers.SerializerMethodField()
def get_author_name(self, obj):
return f"{obj.author.first_name} {obj.author.last_name}"
# 3. Use serializer validation
def validate_isbn(self, value):
if Book.objects.filter(isbn=value).exists():
raise serializers.ValidationError("ISBN already exists")
return value
# 1. Use dependency injection for reusable logic
def get_current_user(request):
return request.user if request.user.is_authenticated else None
@api.get("/books")
def list_books(request, user=Depends(get_current_user)):
if user:
return Book.objects.filter(owner=user)
return Book.objects.filter(is_public=True)
# 2. Use response models for consistent output
class BookResponse(Schema):
id: int
title: str
author: str
created_at: datetime
@api.get("/books/{book_id}", response=BookResponse)
def get_book(request, book_id: int):
return get_object_or_404(Book, id=book_id)
# 3. Handle errors gracefully
@api.exception_handler(ValidationError)
def validation_errors(request, exc):
return api.create_response(
request,
{"errors": exc.errors()},
status=422
)
# Use select_related for foreign keys
books = Book.objects.select_related('author', 'publisher')
# Use prefetch_related for many-to-many
books = Book.objects.prefetch_related('genres', 'reviews')
# Implement database indexing
class Book(models.Model):
title = models.CharField(max_length=200, db_index=True)
isbn = models.CharField(max_length=13, unique=True)
class Meta:
indexes = [
models.Index(fields=['author', 'published_date']),
models.Index(fields=['title', 'author']),
]
# DRF with cache
from django.core.cache import cache
class BookViewSet(viewsets.ModelViewSet):
def list(self, request):
cache_key = f"books_list_{hash(str(request.GET))}"
cached_result = cache.get(cache_key)
if cached_result:
return Response(cached_result)
response = super().list(request)
cache.set(cache_key, response.data, timeout=300)
return response
# Ninja with cache
@api.get("/books")
def list_books(request):
cache_key = "books_list"
cached_books = cache.get(cache_key)
if cached_books:
return cached_books
books = list(Book.objects.all())
cache.set(cache_key, books, timeout=300)
return books
Both Django REST Framework and Django Ninja are excellent choices for building APIs, but they serve different needs:
Choose DRF if you:
Choose Ninja if you:
The decision ultimately depends on your project requirements, team expertise, and long-term maintenance considerations. Both frameworks will continue to evolve, with Ninja likely gaining more adoption as teams embrace modern Python practices.
For new projects where performance and developer experience are priorities, Django Ninja offers compelling advantages. For established applications or teams requiring battle-tested stability, Django REST Framework remains the safer choice.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.