turbodrf
TurboDRF - Dead simple Django REST API generator with role-based permissions
Turn your Django models into fully-featured REST APIs with a mixin and a configuration method. Zero boilerplate.
Overview
TurboDRF is a Django REST Framework mixin-based library that automatically generates CRUD API endpoints for your models. Unlike traditional DRF setups requiring ViewSets and serializers, TurboDRF uses a simple mixin pattern where you declare your model inherits from TurboDRFMixin and define a turbodrf() configuration method.
Key Features:
- Automatic CRUD endpoints from model declaration
- Role-based access control (RBAC)
- Field-level permissions
- Built-in search, filtering, ordering, and pagination
- Nested field support for relationships
- Client-side field selection (
?fields=) - Auto-generated API documentation (Swagger UI, ReDoc)
- Performance optimizations with compiled read path
- Security: sensitive fields deny-list, FK injection defense, startup safety gates
Installation
PyPI
pip install turbodrf
# Optional: faster JSON rendering (7x faster than stdlib)
pip install turbodrf[fast]
GitHub
pip install git+https://github.com/AlexanderCollins/TurboDRF.git
Requirements
- Python >=3.10 (tested: 3.10, 3.11, 3.12, 3.13, 3.14)
- Django >=4.2 (tested: 4.2, 5.2, 6.0)
- Django REST Framework >=3.14.0
- drf-yasg >=1.21.0, django-filter >=23.0
- Optional extras:
turbodrf[fast](msgspec/orjson ~7x faster JSON),turbodrf[allauth](django-allauth >=0.57.0)
Verified against TurboDRF v0.5.1 (2026-07-12).
Quick Start
1. Add to INSTALLED_APPS
# settings.py
INSTALLED_APPS = [
# Django apps
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party apps
'rest_framework',
'django_filters',
# TurboDRF
'turbodrf',
# Your apps
'myapp',
]
2. Add the mixin to your model
# myapp/models.py
from django.db import models
from turbodrf.mixins import TurboDRFMixin
class Book(models.Model, TurboDRFMixin):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
published_date = models.DateField()
# Define searchable fields
searchable_fields = ['title', 'author']
@classmethod
def turbodrf(cls):
return {
'fields': ['title', 'author', 'price', 'published_date']
}
3. Add the router
# urls.py
from django.contrib import admin
from django.urls import path, include
from turbodrf.router import TurboDRFRouter
router = TurboDRFRouter()
urlpatterns = [
# Admin
path('admin/', admin.site.urls),
# API with auto-configured documentation
path('api/', include(router.urls)),
]
4. Configure TurboDRF roles
# settings.py
TURBODRF_ROLES = {
'admin': [
# Model-level permissions
'myapp.book.read',
'myapp.book.create',
'myapp.book.update',
'myapp.book.delete',
# Field-level permissions
'myapp.book.price.read',
'myapp.book.price.write',
],
'editor': [
'myapp.book.read',
'myapp.book.update',
'myapp.book.price.read', # Read-only access to price
],
'viewer': [
'myapp.book.read',
# No access to price field
]
}
5. Extend User Model with Roles
# myapp/apps.py
from django.apps import AppConfig
from django.contrib.auth import get_user_model
class MyAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'myapp'
def ready(self):
User = get_user_model()
def get_user_roles(self):
# Example: Use Django groups as roles
return [group.name for group in self.groups.all()]
if not hasattr(User, 'roles'):
User.add_to_class('roles', property(get_user_roles))
Done! You now have a full REST API at /api/ with:
GET /api/books/ # List all books
POST /api/books/ # Create a new book
GET /api/books/1/ # Get a specific book
PUT /api/books/1/ # Update a book
DELETE /api/books/1/ # Delete a book
Query parameters:
GET /api/books/?search=django # Search
GET /api/books/?author__name=Smith # Filter
GET /api/books/?ordering=-price # Order
GET /api/books/?page=2&page_size=10 # Paginate
GET /api/books/?fields=title,price # Client field selection
Model Configuration
Basic Configuration
@classmethod
def turbodrf(cls):
return {
'enabled': True, # Enable/disable API (default: True)
'endpoint': 'books', # Custom endpoint name (default: pluralized model name)
'fields': ['title', 'author'], # Fields to expose (see below)
'public_access': False, # Allow unauthenticated GET (default: False)
'lookup_field': 'pk', # URL lookup field (default: 'pk', or 'slug')
'compiled': True, # Use compiled read path (default: True)
}
Fields Specification
All database fields:
'fields': '__all__'
Specific fields (same for list and detail):
'fields': ['title', 'author', 'price']
Different fields for list vs detail:
'fields': {
'list': ['title', 'author', 'price'],
'detail': ['title', 'description', 'author', 'author__email', 'price']
}
Nested Fields
Access related model fields with __ notation:
'fields': [
'title',
'author__name', # ForeignKey (1 level)
'author__publisher__name', # Multi-level (2 levels)
'tags__name', # ManyToMany
]
FK fields are flattened in responses (author__name becomes author_name). M2M fields are arrays of objects:
{
"title": "Django for APIs",
"author_name": "William Vincent",
"tags": [{"name": "Python"}, {"name": "Django"}]
}
Maximum nesting depth is 3 by default. Change with TURBODRF_MAX_NESTING_DEPTH in settings.
Property Fields
Model @property methods work in the compiled path:
class Book(models.Model, TurboDRFMixin):
title = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
@property
def display_title(self):
return self.title.upper()
@classmethod
def turbodrf(cls):
return {
'fields': ['title', 'price', 'display_title']
}
Properties that access related objects (e.g., self.author.name) won't work in the compiled path — use author__name in the field config instead.
List/Detail Field Separation
class Book(models.Model, TurboDRFMixin):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
@classmethod
def turbodrf(cls):
return {
'fields': {
'list': ['title', 'author', 'price'],
'detail': ['title', 'author', 'description', 'price']
}
}
Permissions
Permission Modes
TurboDRF supports three permission modes:
No permissions (development):
TURBODRF_DISABLE_PERMISSIONS = TrueDjango default permissions:
TURBODRF_USE_DEFAULT_PERMISSIONS = TrueRole-based permissions (default):
TURBODRF_ROLES = { 'admin': [ 'myapp.book.read', 'myapp.book.create', 'myapp.book.update', 'myapp.book.delete', 'myapp.book.price.read', 'myapp.book.price.write', ], 'editor': [ 'myapp.book.read', 'myapp.book.update', 'myapp.book.price.read', ], 'viewer': [ 'myapp.book.read', ] }
Permission Format
- Model-level:
app_label.model_name.action(read, create, update, delete) - Field-level:
app_label.model_name.field_name.reador.write
Field Permissions
- If ANY role defines an explicit field rule (e.g.,
price.read), that field requires explicit permission for ALL roles - Fields without explicit rules fall back to model-level permission
- To restrict
pricefor viewers, addprice.readto at least one role (like admin)
How It Works
TurboDRF reads user.roles — a property that returns a list of role names:
# From Django groups
User.add_to_class('roles', property(lambda self: [g.name for g in self.groups.all()]))
# From a JSONField
class User(AbstractUser):
user_roles = models.JSONField(default=list)
@property
def roles(self):
return self.user_roles
Authenticated users with no roles get 403 on all endpoints.
Database-Backed Permissions
For runtime changes without redeployment:
TURBODRF_PERMISSION_MODE = 'database'
TURBODRF_PERMISSION_CACHE_TIMEOUT = 300 # 5 minutes
from turbodrf.models import TurboDRFRole, RolePermission, UserRole
role = TurboDRFRole.objects.create(name='editor')
RolePermission.objects.create(role=role, app_label='books', model_name='book', action='read')
UserRole.objects.create(user=user, role=role)
Nested Field Permissions
Permissions are checked at each level of a nested field path. For author__publisher__name:
- Can user read
authoron Book? - Can user read
publisheron Author? - Can user read
nameon Publisher?
If any level fails, the field is excluded.
Filter Permissions
Users can only filter on fields they have read permission for. Filters on hidden fields are silently ignored.
Custom Actions (v0.5.0)
TurboDRF supports custom actions attached to a model's config, inheriting tenant and predicate scoping:
from turbodrf.decorators import turbodrf_action
class Book(models.Model, TurboDRFMixin):
@classmethod
def turbodrf(cls):
return {
'actions': [
turbodrf_action(detail=True, methods=["post"], url_path="resend")(cls.resend)
],
}
def resend(self, request):
# tenant-scoped; predicate stack enforced
...
Read-Only and HTTP Method Control (v0.5.0)
read_only: True— restricts a model to GET endpoints onlyhttp_methods: ['list', 'retrieve']— whitelist specific HTTP methodsfull_clean: True— runsmodel.full_clean()before save
Computed Fields on Both Read Paths (v0.5.0)
@property fields work on both the standard and compiled read paths via DictProxy. Models with unsupported fields (GenericFK) auto-fallback to the serializer path.
Strict Swagger Role Preview (v0.5.0)
The ?role= query parameter is honored in Swagger only for roles the caller actually holds. A user cannot preview another role's permissions.
Tenancy & Row-Level Access
Multi-Tenant SaaS
class Project(models.Model, TurboDRFMixin):
workspace = models.ForeignKey(Workspace,
owner = models.ForeignKey(User,
@classmethod
def turbodrf(cls):
return {
'tenant_field': 'workspace', # mandatory wall
'owner_field': 'owner', # within-tenant rule
'bypass_owner_roles': ['manager', 'admin'], # roles ignore owner check
'fields': ['title', 'workspace', 'owner'],
}
```python
TURBODRF_TENANT_MODEL = 'accounts.Workspace'
TURBODRF_TENANT_USER_FIELD = 'workspace' # request.user.workspace → tenant
Request Flow
A request GET /api/projects/ from Alice (member at ABC workspace) goes through:
- Permission gate — Alice's role
memberhasapp.project.read. Pass. - Tenant filter (mandatory, applied first, never bypassable):
WHERE project.workspace_id = <Alice's workspace> - Owner filter (Alice has no bypass role, so this layer applies):
AND project.owner_id = <Alice's user id> - Field stripping — Alice's role has read on
title,workspace,ownerbut maybe not all configured fields. Hidden ones are removed from the response.
Quick Recipes
# Multi-tenant SaaS — most common case
{'tenant_field': 'store', 'owner_field': 'customer', 'bypass_owner_roles': ['staff']}
# Personal data app (no tenant)
{'owner_field': 'author', 'bypass_owner_roles': ['admin']}
# Reference data (currencies, country codes — not tenant-scoped)
{'tenancy': 'shared'}
# M2M membership (Slack channels, Linear projects)
{'visibility': [Tenant('workspace'), Members('participants')]}
# Power-form composition (when sugar doesn't fit)
{'visibility': [Tenant('workspace'), Either(Owner('owner'), Members('shared_with'))]}
See docs/tenancy.md for the full predicate vocabulary, hard-fail-at-startup behavior, and 404-vs-403 semantics.
Full Predicate Vocabulary (v0.4.0+)
Predicate primitives in turbodrf.predicates:
Tenant(field='tenant')— mandatory tenant boundary, applied as AND outside the algebra (OR-composition can never escape it)Owner(field='owner')— row belongs torequest.userMembers(field='members')— row's M2M members contains user (read-only: raisesNotImplementedErroron writes)Group(field='group')— row's group field matches user's group (read-only: raisesNotImplementedErroron writes)Either(left, right)— logical OR of two predicatesConditional(q_func, write_validator=...)— custom predicate;write_validatoris mandatory (read-only if omitted: raisesNotImplementedErroron writes)Custom(q_func, write_validator=...)— fully custom;write_validatorrequired
Tenant()insideEither()is prohibited — the tenant boundary must not be OR-able away.
Security
Sensitive Fields
Fields like password, token, and secret_key are never exposed:
TURBODRF_SENSITIVE_FIELDS = [
'password', 'password_hash', 'secret_key', 'api_key',
'token', 'access_token', 'refresh_token', 'session_key',
]
Fail-Closed Design
If a permission check fails due to an error, access is denied. TurboDRF never grants access on exception.
Error Responses
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'turbodrf.exceptions.turbodrf_exception_handler',
}
{
"error": {
"status": 403,
"code": "permission_denied",
"message": "You do not have permission to perform this action."
}
}
Security Gates
TurboDRF runs 5 startup safety passes; each has a kill-switch setting:
- Tenancy declaration — every model declares
tenant_field/visibility/tenancy: 'shared'(kill-switch:TURBODRF_REQUIRE_TENANCY=False) - Compiled-path FK safety — validates FK annotations can't bypass predicates (kill-switch:
TURBODRF_ALLOW_UNSAFE_COMPILED_FK) - Compiled-path M2M safety — validates M2M target traversal (kill-switch:
TURBODRF_ALLOW_UNSAFE_COMPILED_M2M) - Filter traversal safety — validates searchable fields can't leak via filter joins (kill-switch:
TURBODRF_ALLOW_UNSAFE_FILTER_TRAVERSAL) - Custom predicate write-safety —
Custom/Conditionalmust carry explicitwrite_validator(kill-switch:TURBODRF_ALLOW_UNSAFE_CUSTOM_WRITE) - Permission string typo check — catches typos in
TURBODRF_ROLESwith difflib suggestions (kill-switch:TURBODRF_ALLOW_UNKNOWN_PERMISSIONS)
Additional protections:
Members,Group, andConditionalpredicates raiseNotImplementedErroron writes (read-only enforcement)TURBODRF_MAX_FILTER_VALUE_LENGTH(default 1000) caps filter value lengthTURBODRF_LOG_UNRESTRICTED_CUSTOM(defaultTrue) logs when unrestricted custom predicates run- FK injection defense: every FK in create/update bodies validated against the target's predicate stack; cross-tenant targets return 400 indistinguishable from nonexistent
These gates refuse to boot if unsafe configurations are detected, preventing cross-permission read leaks.
Documentation
Auto-generated Swagger UI and ReDoc:
- Swagger UI:
/api/swagger/ - ReDoc:
/api/redoc/
Disable in production:
TURBODRF_ENABLE_DOCS = False
Management Commands
# Validate configuration
python manage.py turbodrf_check
# Performance benchmark
python manage.py turbodrf_benchmark
# Explain query execution
python manage.py turbodrf_explain
Settings Reference
Key settings:
| Setting | Description |
|---|---|
TURBODRF_ROLES |
Role-based permissions dict |
TURBODRF_DISABLE_PERMISSIONS |
Disable all permissions |
TURBODRF_USE_DEFAULT_PERMISSIONS |
Use Django default permissions |
TURBODRF_PERMISSION_MODE |
'static' or 'database' |
TURBODRF_PERMISSION_CACHE_TIMEOUT |
Permission cache TTL (seconds) |
TURBODRF_TENANT_MODEL |
Default tenant model |
TURBODRF_TENANT_USER_FIELD |
User's tenant field |
TURBODRF_SENSITIVE_FIELDS |
Fields to hide from all users |
TURBODRF_ENABLE_DOCS |
Enable Swagger/ReDoc |
TURBODRF_MAX_NESTING_DEPTH |
Max nested field depth |
TURBODRF_USE_FILTERS |
Enable Django filters |
TURBODRF_DEFAULT_PAGE_SIZE |
Default pagination page size |
TURBODRF_REQUIRE_TENANCY |
Force tenancy declaration on all models (default True) |
TURBODRF_ALLOW_UNSAFE_COMPILED_FK |
Kill-switch for compiled FK safety gate |
TURBODRF_ALLOW_UNSAFE_COMPILED_M2M |
Kill-switch for compiled M2M safety gate |
TURBODRF_ALLOW_UNSAFE_FILTER_TRAVERSAL |
Kill-switch for filter traversal safety gate |
TURBODRF_ALLOW_UNSAFE_CUSTOM_WRITE |
Kill-switch for custom predicate write-safety gate |
TURBODRF_ALLOW_UNKNOWN_PERMISSIONS |
Kill-switch for permission string typo check |
TURBODRF_MAX_FILTER_VALUE_LENGTH |
Max filter value length (default 1000) |
TURBODRF_LOG_UNRESTRICTED_CUSTOM |
Log unrestricted custom predicates (default True) |
Examples
Complete CRUD API
models.py:
from django.db import models
from turbodrf.mixins import TurboDRFMixin
class Author(models.Model, TurboDRFMixin):
name = models.CharField(max_length=100)
email = models.EmailField()
@classmethod
def turbodrf(cls):
return {
'fields': ['name', 'email']
}
class Book(models.Model, TurboDRFMixin):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author,
price = models.DecimalField(max_digits=10, decimal_places=2)
@classmethod
def turbodrf(cls):
return {
'fields': {
'list': ['title', 'author__name'],
'detail': ['title', 'author__name', 'author__email', 'price']
}
}
settings.py:
INSTALLED_APPS = [
'rest_framework',
'turbodrf',
'myapp',
]
TURBODRF_ROLES = {
'admin': [
'myapp.book.read',
'myapp.book.create',
'myapp.book.update',
'myapp.book.delete',
'myapp.book.price.read',
'myapp.book.price.write',
],
'editor': [
'myapp.book.read',
'myapp.book.update',
'myapp.book.price.read',
],
'viewer': [
'myapp.book.read',
]
}
urls.py:
from django.contrib import admin
from django.urls import path, include
from turbodrf.router import TurboDRFRouter
router = TurboDRFRouter()
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include(router.urls)),
]
Best Practices
1. Use Meta Options
Define fields explicitly rather than __all__ for better control.
2. Validate Input
Use Django's form validation or custom validators:
Integrations
TurboDRF ships optional, experimental integrations (all settings-gated, marked experimental in docs):
- Sentry — security-event breadcrumbs
- Keycloak — role mapping (
STRICT_ROLES=Truedefault) - django-allauth — group→role mapping (
pip install turbodrf[allauth]) - drf-api-tracking — request logging
Fast JSON: pip install turbodrf[fast] adds msgspec (~7x faster serialization; orjson auto-detected if present).
AI Agent Guidance
The TurboDRF repository ships an AGENTS.md with canonical guidance for AI coding agents — the "what never to do" list (don't override get_queryset, don't use Model.objects.all() in custom actions, don't widen serializer fields, don't use tenancy: 'shared' as a test workaround) plus invalidate_user_permissions() cache API and testing patterns.
- Repo AGENTS.md: https://github.com/AlexanderCollins/TurboDRF/blob/main/AGENTS.md
Docs status: The readthedocs and GitHub Pages sites are currently 404. The repo
docs/folder is the only current documentation source.
References
- GitHub Repository: https://github.com/AlexanderCollins/TurboDRF
- PyPI Package: https://pypi.org/project/turbodrf/
- Documentation: https://github.com/AlexanderCollins/TurboDRF/tree/main/docs
- AI Agent Guide (AGENTS.md): https://github.com/AlexanderCollins/TurboDRF/blob/main/AGENTS.md def validate_title(value): if Book.objects.filter(title=value).exclude(pk=self.instance.pk).exists(): raise serializers.ValidationError("Title already exists") return value
### 3. Use Permissions
Restrict access appropriately:
```python
TURBODRF_ROLES = {
'public': ['myapp.book.read'],
'staff': [
'myapp.book.read',
'myapp.book.create',
'myapp.book.update',
'myapp.book.delete',
],
}
4. Filter Usage
Users can only filter on fields they have read permission for.
5. Secure Sensitive Data
Always include sensitive fields in the deny-list:
TURBODRF_SENSITIVE_FIELDS = [
'password', 'token', 'api_key', 'secret_key',
]
Troubleshooting
Import Errors
If you get import errors, ensure TurboDRF is properly installed:
pip list | grep turbodrf
No API Endpoints
If no endpoints appear, check that:
- Your models inherit from
TurboDRFMixin - Models have a
turbodrf()classmethod - The model is not disabled (
'enabled': False)
Permission Denied
If you get 403 errors:
- Check your user's roles
- Verify role permissions in
TURBODRF_ROLES - Ensure the User model has a
rolesproperty
Compiled Path Safety Issues
If startup gate fires due to M2M/FK traversals:
- Drop the path from the parent's
turbodrf()fieldslist - Set
'compiled': Falseon the parent model - Strip the target's row-level rules if genuinely public
TURBODRF_ALLOW_UNSAFE_COMPILED_M2M = True(not recommended)
References
- GitHub Repository: https://github.com/AlexanderCollins/TurboDRF
- PyPI Package: https://pypi.org/project/turbodrf/
- Documentation: https://github.com/AlexanderCollins/TurboDRF/tree/main/docs