Hi Django community! ![]()
I’ve been working on a framework to automate REST API boilerplate in Django projects and would love your feedback.
The Problem
Building REST APIs in Django typically requires writing the same patterns repeatedly:
- Serializer classes for input/output
- ViewSets or view functions
- Filtering and pagination logic
- Authentication setup
- M2M relationship endpoints
For each model, this means 150-200 lines of mostly boilerplate code.
The Solution: django-ninja-aio-crud
I built a framework that generates complete async CRUD endpoints from Django models with minimal configuration.
Quick Example
# models.py
from ninja_aio.models import ModelSerializer
class Book(ModelSerializer):
title = models.CharField(max_length=120)
published = models.BooleanField(default=True)
class ReadSerializer:
fields = ["id", "title", "published"]
class CreateSerializer:
fields = ["title", "published"]
class UpdateSerializer:
optionals = [("title", str), ("published", bool)]
# views.py
from ninja_aio import NinjaAIO
from ninja_aio.views import APIViewSet
api = NinjaAIO()
@api.viewset(Book)
class BookViewSet(APIViewSet):
pass
Result: 5 CRUD endpoints ready:
GET /book/- List with paginationPOST /book/- CreateGET /book/{pk}- RetrievePATCH /book/{pk}- UpdateDELETE /book/{pk}- Delete
Visit /api/docs and you’ll see all endpoints with OpenAPI documentation.
Key Features
Core:
Full async/await support (built on Django Ninja)
Automatic Pydantic schema generation
Per-method authentication (auth,get_auth,post_auth, etc.)
Built-in filtering and pagination
M2M relationship endpoints with filtering
Advanced:
Pydantic validators directly on serializer classes
Lifecycle hooks (before_save,after_save,on_delete)
Reverse relation serialization
Custom endpoints via decorators
Support for existing Django models (Meta-driven Serializer)
Two Patterns for Different Use Cases
Pattern 1: ModelSerializer (new projects)
For new projects, inherit from ModelSerializer:
class Article(ModelSerializer):
title = models.CharField(max_length=200)
author = models.ForeignKey(User, on_delete=models.CASCADE)
class ReadSerializer:
fields = ["id", "title", "author"]
class CreateSerializer:
fields = ["title", "author"]
Pattern 2: Meta-driven Serializer (existing projects)
For existing Django models, use the Meta-driven approach without changing model base classes:
from ninja_aio.models import serializers
class ArticleSerializer(serializers.Serializer):
class Meta:
model = models.Article
schema_in = serializers.SchemaModelConfig(fields=["title", "author"])
schema_out = serializers.SchemaModelConfig(fields=["id", "title", "author"])
schema_update = serializers.SchemaModelConfig(
optionals=[("title", str), ("author", int)]
)
# Then use in ViewSet
@api.viewset(models.Article)
class ArticleViewSet(APIViewSet):
serializer_class = ArticleSerializer
This means you can add REST APIs to existing Django projects without refactoring your models!
Example: Complete Blog API
Here’s a more realistic example with relationships:
from ninja_aio.models import ModelSerializer
from ninja_aio.views import APIViewSet
from ninja_aio.schemas import M2MRelationSchema
from ninja_aio import NinjaAIO
class Author(ModelSerializer):
name = models.CharField(max_length=200)
email = models.EmailField(unique=True)
class ReadSerializer:
fields = ["id", "name", "email", "articles"] # articles = reverse relation
class CreateSerializer:
fields = ["name", "email"]
class Tag(ModelSerializer):
name = models.CharField(max_length=50, unique=True)
class ReadSerializer:
fields = ["id", "name"]
class Article(ModelSerializer):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="articles")
tags = models.ManyToManyField(Tag, related_name="articles")
is_published = models.BooleanField(default=False)
class ReadSerializer:
fields = ["id", "title", "content", "author", "tags", "is_published"]
class CreateSerializer:
fields = ["title", "content", "author"]
class UpdateSerializer:
optionals = [("title", str), ("content", str), ("is_published", bool)]
api = NinjaAIO()
@api.viewset(Author)
class AuthorViewSet(APIViewSet):
pass
@api.viewset(Tag)
class TagViewSet(APIViewSet):
pass
@api.viewset(Article)
class ArticleViewSet(APIViewSet):
# Add query filtering
query_params = {
"author": (int, None),
"is_published": (bool, None),
"title": (str, None)
}
# Add M2M relationship endpoints
m2m_relations = [
M2MRelationSchema(
model=Tag,
related_name="tags",
filters={"name": (str, "")}
)
]
async def query_params_handler(self, queryset, filters):
if filters.get("author"):
queryset = queryset.filter(author_id=filters["author"])
if filters.get("is_published") is not None:
queryset = queryset.filter(is_published=filters["is_published"])
if filters.get("title"):
queryset = queryset.filter(title__icontains=filters["title"])
return queryset
async def tags_query_params_handler(self, queryset, filters):
if filters.get("name"):
queryset = queryset.filter(name__icontains=filters["name"])
return queryset
This creates:
- Full CRUD for Author, Tag, and Article
- Nested serialization (articles include author and tags objects)
- Filtering:
GET /article/?author=1&is_published=true&title=django - M2M endpoints:
GET /article/{pk}/tag?name=pythonandPOST /article/{pk}/tag/(with{"add": [1, 2], "remove": [3]})
Schema Validators (Pydantic)
You can add Pydantic validators directly on serializer classes:
from pydantic import field_validator, model_validator
class Book(ModelSerializer):
title = models.CharField(max_length=120)
description = models.TextField(blank=True)
class CreateSerializer:
fields = ["title", "description"]
@field_validator("title")
@classmethod
def validate_title_min_length(cls, v):
if len(v) < 3:
raise ValueError("Title must be at least 3 characters")
return v
class UpdateSerializer:
optionals = [("title", str), ("description", str)]
@field_validator("title")
@classmethod
def validate_title_not_empty(cls, v):
if v is not None and len(v.strip()) == 0:
raise ValueError("Title cannot be blank")
return v
class ReadSerializer:
fields = ["id", "title", "description"]
@model_validator(mode="after")
def enrich_output(self):
# Transform output data if needed
return self
The framework automatically collects these validators and applies them to the generated Pydantic schemas.
Authentication Example
Built-in async JWT authentication:
from ninja_aio.auth import AsyncJwtBearer
from joserfc import jwk
class JWTAuth(AsyncJwtBearer):
jwt_public = jwk.RSAKey.import_key("-----BEGIN PUBLIC KEY----- ...")
jwt_alg = "RS256"
claims = {"sub": {"essential": True}}
async def auth_handler(self, request):
user_id = self.dcd.claims.get("sub")
return await User.objects.aget(id=user_id)
@api.viewset(Book)
class SecureBookViewSet(APIViewSet):
auth = [JWTAuth()] # All operations require auth
get_auth = None # Except list/retrieve (public)
post_auth = [JWTAuth()] # Create requires auth
Granular per-method auth control without repeating decorators.
Lifecycle Hooks
Available hooks for custom logic:
class Article(ModelSerializer):
# ... fields ...
async def before_save(self, instance, payload, request):
# Called before any save
instance.slug = slugify(instance.title)
async def on_create_after_save(self, instance, payload, request):
# Called after create
await send_notification(f"New article: {instance.title}")
async def custom_actions(self, payload):
# Handle custom fields from CreateSerializer
if payload.get("send_email"):
await send_email_to_subscribers(self)
async def on_delete(self, request):
# Called before deletion
await log_deletion(self.id, request.user)
Quality & Performance
Testing & Quality:
- 98%+ test coverage via pytest
- SonarCloud quality gate passing
- Zero critical security issues
- Type hints throughout
Compatibility:
- Python 3.10, 3.11, 3.12, 3.13, 3.14
- Django Ninja 1.3, 1.4, 1.5
- Django 4.2+ (async ORM support required)
Performance:
- Full async/await (no blocking calls)
- orjson for fast JSON serialization
- Automated performance benchmarks in CI
Current benchmark results (median):
- Single object serialization: ~0.5-1ms
- Bulk serialization (100 objects): ~5-15ms
- Full CRUD endpoint cycle: ~2-8ms
Installation & Quick Start
pip install django-ninja-aio-crud
# settings.py
INSTALLED_APPS = [
# ...
'ninja_aio',
]
# models.py
from ninja_aio.models import ModelSerializer
class Book(ModelSerializer):
title = models.CharField(max_length=120)
class ReadSerializer:
fields = ["id", "title"]
class CreateSerializer:
fields = ["title"]
# urls.py
from ninja_aio import NinjaAIO
from ninja_aio.views import APIViewSet
from .models import Book
api = NinjaAIO()
@api.viewset(Book)
class BookViewSet(APIViewSet):
pass
urlpatterns = [
path("api/", api.urls),
]
Visit http://localhost:8000/api/docs - your CRUD endpoints are ready!
Resources
- Documentation: https://django-ninja-aio.com (comprehensive guides and API reference)
- GitHub: GitHub - caspel26/django-ninja-aio-crud: Based on Django Ninja, Django ninja aio crud is a framework which implement an Async dynamic Model CRUD. It comes out with built-in authentication classes, class based views and dynamic CRUD, fast and async.
Looking for Feedback
I’d genuinely appreciate feedback from the Django community:
Use cases:
- What scenarios would this be most/least useful for?
- Are there Django project patterns where this wouldn’t fit?
Features:
- What’s missing that would make this production-ready for you?
- Are there common CRUD patterns not covered?
API design:
- Are the serializer patterns intuitive?
- Would you prefer different configuration approaches?
Integration:
- How would this fit into your current Django workflow?
- Any concerns about mixing this with existing DRF or custom views?
Documentation:
- What examples or guides would be most helpful?
- What use cases should be documented better?
Why I Built This
I’ve been building Django APIs for several years, and I found myself writing the same CRUD patterns over and over. Each new model meant:
- Creating 3-4 serializer classes
- Writing ViewSet or view functions
- Configuring the same filters
- Setting up the same auth patterns
- Adding pagination boilerplate
Django REST Framework is comprehensive but verbose. Django Ninja improved the developer experience significantly, but CRUD still required substantial boilerplate.
I wanted something that:
- Eliminates repetitive patterns while staying flexible
- Stays fully async (critical for modern Django)
- Doesn’t hide or replace Django’s ORM
- Integrates cleanly with existing projects
- Provides good performance out of the box
This project is the result. It’s opinionated about CRUD patterns (automatic endpoints, standard schemas) but flexible about business logic (hooks, custom endpoints, query handlers).
Design Philosophy
What this framework does:
Automates repetitive CRUD boilerplate
Provides sensible defaults
Makes common patterns easy
What this framework doesn’t do:
Replace Django’s ORM
Hide complexity behind magic
Force you into specific architectures
You still write Django code. The framework just reduces repetition.
Comparison to Alternatives
vs Django REST Framework:
- Less verbose (10 lines vs 150+ for basic CRUD)
- Fully async (DRF has partial async support)
- Pydantic schemas (instead of DRF serializers)
- Less customizable than DRF (trade-off for simplicity)
vs Django Ninja (base):
- Automatic CRUD generation (Ninja requires manual endpoints)
- Built-in filtering and pagination
- Schema-on-model pattern
- Same underlying framework (built on Ninja)
vs FastAPI + SQLAlchemy:
- Native Django integration
- Uses Django ORM (async)
- Django-native patterns
- Better for Django projects specifically
Limitations & Trade-offs
When to use this:
- Standard CRUD-heavy APIs
- New Django projects
- Microservices with simple data models
- Rapid prototyping
When not to use this:
- Highly custom endpoint logic (though hooks may suffice)
- Complex GraphQL-style nested mutations
- Projects requiring DRF ecosystem plugins
Trade-offs:
- Less flexible than writing views manually
- Adds a layer of abstraction
- Learning curve for the framework patterns
I believe the trade-offs are worth it for most CRUD use cases, but I’d love to hear if you disagree!
Contributing
The project is MIT-licensed and open to contributions:
Bug reports and feature requests welcome on GitHub
Pull requests appreciated (CONTRIBUTING.md in repo)
Documentation improvements needed
Real-world usage feedback valuable
If you try it and run into issues or missing features, please open an issue!
Thanks
Thanks for reading this far! I really value this community’s input.
If you try django-ninja-aio-crud, I’d love to hear:
- What worked well
- What didn’t work
- What you’d change
- Whether it fits your workflow
Happy to answer any questions or discuss the approach in detail.
If you find this useful, consider:
Starring the GitHub repo
Trying it in a project
Sharing feedback
Contributing improvements