TurboDRF
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 True to see all available fields while building/testing
- Production: Keep False so 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)
Keycloak / OpenID Connect
Integrate with Keycloak or any OpenID Connect provider for SSO authentication:
pip install social-auth-app-django
1. Configure Django settings:
# settings.py
INSTALLED_APPS = [
'social_django',
'turbodrf',
]
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'turbodrf.integrations.keycloak.KeycloakRoleMiddleware',
]
# Enable Keycloak integration
TURBODRF_KEYCLOAK_INTEGRATION = True
# Configure role claim path in ID token
TURBODRF_KEYCLOAK_ROLE_CLAIM = 'realm_access.roles' # or 'roles' for simple claims
# Map Keycloak roles to TurboDRF roles
TURBODRF_KEYCLOAK_ROLE_MAPPING = {
'realm-admin': 'admin',
'content-editor': 'editor',
'basic-user': 'viewer',
}
# Social auth configuration
AUTHENTICATION_BACKENDS = [
'social_core.backends.keycloak.KeycloakOAuth2',
'django.contrib.auth.backends.ModelBackend',
]
SOCIAL_AUTH_KEYCLOAK_KEY = 'your-client-id'
SOCIAL_AUTH_KEYCLOAK_SECRET = 'your-client-secret'
SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY = 'your-realm-public-key'
SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL = 'https://your-keycloak/auth/realms/your-realm/protocol/openid-connect/auth'
SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL = 'https://your-keycloak/auth/realms/your-realm/protocol/openid-connect/token'
2. Common role claim paths:
# Simple roles claim
TURBODRF_KEYCLOAK_ROLE_CLAIM = 'roles'
# Token: {"roles": ["admin", "editor"]}
# Realm roles
TURBODRF_KEYCLOAK_ROLE_CLAIM = 'realm_access.roles'
# Token: {"realm_access": {"roles": ["admin", "editor"]}}
# Client roles
TURBODRF_KEYCLOAK_ROLE_CLAIM = 'resource_access.my-client.roles'
# Token: {"resource_access": {"my-client": {"roles": ["admin"]}}}
3. Role mapping:
The middleware automatically extracts roles from the ID token and maps them to TurboDRF roles. Unmapped roles pass through unchanged:
# User logs in with Keycloak roles: ['realm-admin', 'developer']
# With mapping: {'realm-admin': 'admin'}
# TurboDRF sees: ['admin', 'developer']
How it works:
1. User authenticates via Keycloak OAuth2
2. KeycloakRoleMiddleware extracts roles from ID token claims
3. Roles are mapped to TurboDRF roles via TURBODRF_KEYCLOAK_ROLE_MAPPING
4. Mapped roles are assigned to request.user.roles
5. TurboDRF permissions use these roles for access control
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
python 'fields': { 'list': ['id', 'title'], # Minimal 'detail': '__all__' # Complete } -
Add database indexes to filtered fields
python 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. ```python # ✗ 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__icontains` instead
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/.