Dead simple Django REST API generator with role-based permissions
Project description
TurboDRF
The dead simple Django REST Framework API generator with role-based permissions
New project as of May 2025. Built with assistance from Claude.
Transform your Django models into fully-featured REST APIs with just a mixin and a method. Zero boilerplate, maximum power.
Features • Quick Start • Documentation • Examples • Contributing
Installation
pip install turbodrf
Quick Start
1. Add to settings:
INSTALLED_APPS = [
'rest_framework',
'turbodrf',
]
2. Configure your model:
from django.db import models
from turbodrf.mixins import TurboDRFMixin
class Book(models.Model, TurboDRFMixin):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
price = models.DecimalField(max_digits=10, decimal_places=2)
searchable_fields = ['title']
@classmethod
def turbodrf(cls):
return {
'fields': {
'list': ['title', 'author__name', 'price'],
'detail': ['title', 'author__name', 'author__email', 'price']
}
}
3. Configure URLs:
from turbodrf.router import TurboDRFRouter
router = TurboDRFRouter()
urlpatterns = [
path('api/', include(router.urls)),
]
You now have a complete REST API:
GET /api/books/ # List all books
GET /api/books/1/ # Get book detail
POST /api/books/ # Create book
PUT /api/books/1/ # Update book
DELETE /api/books/1/ # Delete book
# Filtering, search, pagination
GET /api/books/?search=django
GET /api/books/?price__lt=20
GET /api/books/?page=2&page_size=50
Features
- Automatic API generation from models
- Role-based permissions with field-level control
- Nested field access (
author__name,category__parent__title) - Full-text search on configured fields
- Advanced filtering with Django lookups (
__gte,__icontains, etc.) - OR filtering for complex queries
- Pagination with metadata
- Auto-generated API docs (Swagger/ReDoc)
- Query optimization (automatic
select_related)
Configuration
Model Configuration
The turbodrf() method configures the API:
@classmethod
def turbodrf(cls):
return {
'enabled': True, # Enable/disable API
'endpoint': 'books', # Custom endpoint name (default: pluralized model name)
'fields': '__all__', # All fields
# OR
'fields': ['title', 'author__name'], # Specific fields
# OR
'fields': {
'list': ['title'], # Fields for list view
'detail': '__all__' # Fields for detail view
},
'public_access': True, # Allow unauthenticated GET requests
'lookup_field': 'slug', # Custom lookup field (default: pk)
}
Nested Fields
Access related model fields with __ notation:
'fields': [
'title',
'author__name', # ForeignKey (1 level)
'author__bio',
'category__parent__name', # Multi-level (2 levels)
]
Response format (fields are flattened):
{
"title": "Django for APIs",
"author": 2,
"author_name": "William Vincent",
"author_bio": "Django expert...",
"category_parent_name": "Programming"
}
Nesting Depth Limit:
Default maximum nesting depth is 3 levels (e.g., author__publisher__parent__name).
# settings.py
TURBODRF_MAX_NESTING_DEPTH = 3 # Default
# Examples:
'author__name' # ✓ Valid (1 level)
'author__publisher__name' # ✓ Valid (2 levels)
'author__publisher__parent__name' # ✓ Valid (3 levels)
'a__b__c__d__e' # ✗ Exceeds limit (4 levels)
⚠️ WARNING: Increasing TURBODRF_MAX_NESTING_DEPTH beyond 3 is UNSUPPORTED and may cause:
- Performance degradation
- Security vulnerabilities
- Unexpected behavior
- Increased database queries
Nested Field Permissions:
Permissions are checked at each level of the relationship chain:
# For field: author__publisher__revenue
# Requires ALL of:
# 1. books.book.author.read (or books.book.read)
# 2. authors.author.publisher.read (or authors.author.read)
# 3. publishers.publisher.revenue.read (or publishers.publisher.read)
If permission is denied at any level, the entire field is excluded.
ManyToMany Fields
TurboDRF fully supports M2M relationships with clean array-of-objects serialization:
Model Definition:
class Tag(models.Model):
name = models.CharField(max_length=50)
slug = models.SlugField()
category = models.CharField(max_length=50)
class Article(models.Model, TurboDRFMixin):
title = models.CharField(max_length=200)
tags = models.ManyToManyField(Tag, related_name='articles')
@classmethod
def turbodrf(cls):
return {
'fields': {
'list': ['title', 'tags__name'],
'detail': [
'title',
'tags__name',
'tags__slug',
'tags__category',
],
}
}
Response Format:
M2M fields serialize as arrays of objects (not flat fields like ForeignKey):
{
"title": "My Article",
"tags": [
{"name": "Python", "slug": "python", "category": "programming"},
{"name": "Django", "slug": "django", "category": "framework"}
]
}
Filtering M2M Fields:
M2M fields support multiple lookups:
# Filter by tag ID (exact match)
GET /api/articles/?tags=1
# Filter by multiple tag IDs (OR logic)
GET /api/articles/?tags__in=1,2,3
# Check if article has no tags
GET /api/articles/?tags__isnull=true
# Filter by nested field
GET /api/articles/?tags__name__icontains=python
GET /api/articles/?tags__slug=django
GET /api/articles/?tags__category=programming
M2M Field Permissions:
Permissions work the same as ForeignKey - checked at each level:
TURBODRF_ROLES = {
'viewer': [
'articles.article.read',
'articles.article.tags.read', # Permission to see tags field
'tags.tag.read', # Permission to access Tag model
'tags.tag.name.read', # Permission to see tag.name
# NO permission on tag.slug or tag.category
]
}
Result:
{
"title": "My Article",
"tags": [
{"name": "Python"}, // Only 'name' field (has permission)
{"name": "Django"} // slug and category excluded (no permission)
]
}
Nesting Depth for M2M:
M2M fields respect the same nesting depth limits (default 3 levels):
'tags__name' # ✓ Valid (1 level)
'tags__category__parent' # ✓ Valid (2 levels)
'tags__a__b__c__d' # ✗ Exceeds limit (4 levels)
Performance:
- M2M queries use Django's
prefetch_relatedfor efficiency - Permission snapshots are O(1) for M2M fields (same as regular fields)
- No N+1 query issues when properly configured
Search
Define searchable fields:
class Book(models.Model, TurboDRFMixin):
title = models.CharField(max_length=200)
description = models.TextField()
searchable_fields = ['title', 'description']
Usage:
GET /api/books/?search=django
Filtering
All model fields are filterable with Django lookups:
# Exact match
GET /api/books/?author=1
# Comparisons
GET /api/books/?price__gte=10&price__lte=50
# Text search
GET /api/books/?title__icontains=python
# Date filtering
GET /api/books/?published_date__year=2023
# Related fields
GET /api/books/?author__name__istartswith=smith
Filter Permissions:
Users can only filter on fields they have read permission for. Nested filter fields are validated at each level:
# Filter: ?author__publisher__revenue__gte=1000000
# Requires read permission on:
# 1. Book.author
# 2. Author.publisher
# 3. Publisher.revenue
Filters on unpermitted fields are silently ignored. This prevents information leakage through filter-based attacks.
OR Filtering
Use _or suffix for OR queries:
# Title is "Django" OR "Python"
GET /api/books/?title_or=Django&title_or=Python
# With lookups
GET /api/books/?title__icontains_or=django&title__icontains_or=python
# Combined with AND
GET /api/books/?title_or=Django&title_or=Python&price__lt=50
Permissions
TurboDRF supports three permission modes:
1. No Permissions (Development)
# settings.py
TURBODRF_DISABLE_PERMISSIONS = True
2. Django Default Permissions
Standard Django permissions (add_, change_, delete_, view_):
# settings.py
TURBODRF_USE_DEFAULT_PERMISSIONS = True
Grant permissions:
from django.contrib.auth.models import Permission
user.user_permissions.add(
Permission.objects.get(codename='view_book'),
Permission.objects.get(codename='change_book')
)
3. TurboDRF Role-Based Permissions (Default)
Field-level permissions with role management.
Permission format:
- Model-level:
app.model.action(read, create, update, delete) - Field-level:
app.model.field.permission(read, write)
Static Configuration
# settings.py
TURBODRF_ROLES = {
'admin': [
'books.book.read',
'books.book.create',
'books.book.update',
'books.book.delete',
],
'editor': [
'books.book.read',
'books.book.update',
'books.book.price.read', # Can see price
# No price.write - can't change price
],
'viewer': [
'books.book.read',
'books.book.title.read',
# No price permission - can't see price
],
}
Add roles to users:
# Option 1: Property on User model
from django.contrib.auth import get_user_model
User = get_user_model()
def get_user_roles(self):
return [group.name for group in self.groups.all()]
User.add_to_class('roles', property(get_user_roles))
# Option 2: Custom User model
class User(AbstractUser):
user_roles = models.JSONField(default=list)
@property
def roles(self):
return self.user_roles
Database-Backed Permissions
For runtime permission changes without code deployment:
# settings.py
TURBODRF_PERMISSION_MODE = 'database'
TURBODRF_PERMISSION_CACHE_TIMEOUT = 300 # 5 minutes
Add to INSTALLED_APPS:
INSTALLED_APPS = [
'turbodrf', # Must be in INSTALLED_APPS for migrations
]
Run migrations:
python manage.py migrate turbodrf
Create roles and permissions:
from turbodrf.models import TurboDRFRole, RolePermission, UserRole
# Create role
editor = TurboDRFRole.objects.create(name='editor')
# Model-level permissions
RolePermission.objects.create(
role=editor,
app_label='books',
model_name='book',
action='read'
)
# Field-level permissions
RolePermission.objects.create(
role=editor,
app_label='books',
model_name='book',
field_name='price',
permission_type='read' # Can see but not edit
)
# Assign to user
UserRole.objects.create(user=user, role=editor)
Link to Django Groups:
from django.contrib.auth.models import Group
group = Group.objects.create(name='Editors')
editor.django_group = group
editor.save()
Field-level permission logic:
- If field has explicit permissions → check those
- Else → fall back to model-level permissions
- Read: needs
field.readORmodel.read - Write: needs
field.writeORmodel.create/update
Performance: Uses snapshot-based O(1) permission checking with automatic cache invalidation.
Guest Role
Unauthenticated users with public_access: True:
TURBODRF_ROLES = {
'guest': [
'books.book.read',
'books.book.title.read',
# Limited field access for unauthenticated users
],
}
Advanced Usage
Custom ViewSet
from turbodrf.views import TurboDRFViewSet
from rest_framework.decorators import action
class BookViewSet(TurboDRFViewSet):
model = Book
@action(detail=False)
def trending(self, request):
queryset = self.get_queryset().filter(views__gte=1000)
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
# Register manually
router.register('books', BookViewSet, basename='book')
User-Based Filtering
Restrict queryset based on current user:
class UserFilteredViewSet(TurboDRFViewSet):
model = Book
def get_queryset(self):
queryset = super().get_queryset()
if self.request.user.is_authenticated:
return queryset.filter(owner=self.request.user)
return queryset.filter(is_public=True)
Or via model manager:
class BookManager(models.Manager):
def for_user(self, user):
if user.is_superuser:
return self.all()
return self.filter(owner=user)
class Book(models.Model, TurboDRFMixin):
objects = BookManager()
@classmethod
def turbodrf(cls):
return {
'fields': '__all__',
'get_queryset': lambda viewset: cls.objects.for_user(viewset.request.user)
}
Custom Pagination
from turbodrf.views import TurboDRFPagination
class CustomPagination(TurboDRFPagination):
page_size = 50
max_page_size = 200
class BookViewSet(TurboDRFViewSet):
pagination_class = CustomPagination
API Documentation
Swagger UI and ReDoc are enabled by default at:
/api/swagger//api/redoc/
Disable in production:
# settings.py
TURBODRF_ENABLE_DOCS = False
Customize:
from turbodrf.documentation import get_turbodrf_schema_view
schema_view = get_turbodrf_schema_view(
title='My API',
version='v1',
description='API documentation',
)
urlpatterns = [
path('docs/', schema_view.with_ui('swagger')),
]
Swagger Field Visibility
By default, Swagger documentation respects field-level permissions - users only see fields they have access to. For development/testing, you can show all fields regardless of permissions:
# settings.py
# Show all fields in Swagger docs (development only!)
TURBODRF_SWAGGER_SHOW_ALL_FIELDS = True # Default: False
⚠️ Important Notes:
- This setting only affects Swagger/OpenAPI documentation
- The actual API still enforces all permission checks - users cannot access fields they don't have permission for
- Recommended for development environments only to see complete API documentation
- In production, keep this
False(default) so documentation matches actual user permissions
Use Case:
- Development: Set to
Trueto see all available fields while building/testing - Production: Keep
Falseso each role sees only their permitted fields in docs
Integrations
django-allauth
Session-based authentication for SPAs:
pip install turbodrf[allauth]
# settings.py
INSTALLED_APPS = [
'allauth',
'allauth.account',
'allauth.headless',
'turbodrf',
]
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'turbodrf.integrations.allauth.AllAuthRoleMiddleware',
]
TURBODRF_ALLAUTH_INTEGRATION = True
TURBODRF_ALLAUTH_ROLE_MAPPING = {
'Administrators': 'admin',
'Editors': 'editor',
}
Frontend usage with httpOnly cookies:
// Login (sets httpOnly session cookie)
await fetch('/api/auth/login/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ username, password })
});
// Subsequent API calls (cookie sent automatically)
await fetch('/api/books/', { credentials: 'include' });
drf-api-tracking
Request logging:
pip install drf-api-tracking
INSTALLED_APPS = [
'rest_framework_tracking',
'turbodrf',
]
Automatically logs all requests. Access via:
from rest_framework_tracking.models import APIRequestLog
APIRequestLog.objects.filter(user=user)
Testing
from django.test import TestCase
from rest_framework.test import APIClient
class BookAPITest(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user('test', roles=['viewer'])
def test_list_books(self):
self.client.force_authenticate(user=self.user)
response = self.client.get('/api/books/')
self.assertEqual(response.status_code, 200)
Run tests:
pytest
pytest --cov=turbodrf
Performance Tips
-
Use different fields for list/detail views
'fields': { 'list': ['id', 'title'], # Minimal 'detail': '__all__' # Complete }
-
Add database indexes to filtered fields
class Meta: indexes = [ models.Index(fields=['title']), models.Index(fields=['published_date']), ]
-
TurboDRF automatically optimizes queries with
select_relatedfor nested fields
Known Limitations
- Nested writes not supported: TurboDRF only supports nested reads (e.g.,
author__name). Nested resource creation/modification is not supported.# ✗ NOT supported: POST /api/books/ { "title": "New Book", "author": { "name": "New Author", "publisher": {"name": "New Publisher"} } } # ✓ Supported: POST /api/books/ { "title": "New Book", "author": 1 # Foreign key by ID }
- JSONField, BinaryField, FilePathField: Not filterable (included in responses only)
- Reverse relations: Not automatically included (use filtering instead)
- Search on related fields: Use filter with
__icontainsinstead
Migration Guide
From Static to Database Permissions
from turbodrf.models import TurboDRFRole, RolePermission
def migrate_static_to_database():
TURBODRF_ROLES = getattr(settings, 'TURBODRF_ROLES', {})
for role_name, permissions in TURBODRF_ROLES.items():
role, _ = TurboDRFRole.objects.get_or_create(name=role_name)
for perm in permissions:
parts = perm.split('.')
if len(parts) == 3: # Model-level
app_label, model_name, action = parts
RolePermission.objects.get_or_create(
role=role, app_label=app_label,
model_name=model_name, action=action
)
elif len(parts) == 4: # Field-level
app_label, model_name, field_name, ptype = parts
RolePermission.objects.get_or_create(
role=role, app_label=app_label, model_name=model_name,
field_name=field_name, permission_type=ptype
)
License
MIT License. See LICENSE for details.
Acknowledgments
TurboDRF was inspired by fast-drf by https://github.com/iashraful/.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file turbodrf-0.2.0.tar.gz.
File metadata
- Download URL: turbodrf-0.2.0.tar.gz
- Upload date:
- Size: 110.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
abaa453625a0489f57be0930946e22070da97a2ab1f10da7a5986f0b716a0b9b
|
|
| MD5 |
04cd2a35c40753ff0c2b7246b922da4d
|
|
| BLAKE2b-256 |
958b4a2bba74e800c4ce2153bb97fbfc8b028266b952e113a4d17d13b0d649a5
|
Provenance
The following attestation bundles were made for turbodrf-0.2.0.tar.gz:
Publisher:
publish.yml on AlexanderCollins/TurboDRF
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
turbodrf-0.2.0.tar.gz -
Subject digest:
abaa453625a0489f57be0930946e22070da97a2ab1f10da7a5986f0b716a0b9b - Sigstore transparency entry: 790475153
- Sigstore integration time:
-
Permalink:
AlexanderCollins/TurboDRF@d9e625cc1b56ef2b03efc37b8e714276a22397bd -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/AlexanderCollins
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d9e625cc1b56ef2b03efc37b8e714276a22397bd -
Trigger Event:
release
-
Statement type:
File details
Details for the file turbodrf-0.2.0-py3-none-any.whl.
File metadata
- Download URL: turbodrf-0.2.0-py3-none-any.whl
- Upload date:
- Size: 61.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f600d4158efc2ce436a520c204195779ade3313200ef1dee355f70424a36eb4
|
|
| MD5 |
2ba128cc94d82da1edcae0e92e2cec64
|
|
| BLAKE2b-256 |
653fe74d953118526587ec58d4cb6b6925e0c35d72a550e89a644705325c5ba9
|
Provenance
The following attestation bundles were made for turbodrf-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on AlexanderCollins/TurboDRF
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
turbodrf-0.2.0-py3-none-any.whl -
Subject digest:
7f600d4158efc2ce436a520c204195779ade3313200ef1dee355f70424a36eb4 - Sigstore transparency entry: 790475155
- Sigstore integration time:
-
Permalink:
AlexanderCollins/TurboDRF@d9e625cc1b56ef2b03efc37b8e714276a22397bd -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/AlexanderCollins
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d9e625cc1b56ef2b03efc37b8e714276a22397bd -
Trigger Event:
release
-
Statement type: