| name | turbodrf |
| description | TurboDRF - fast Django REST framework with automatic OpenAPI, serializers, views, routers, and caching |
| metadata | {"author":"mte90","version":"1.0.0","tags":["python","django","rest-api","openapi","serializers","caching"]} |
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, Row Level Security (RLS) for Postgres
Installation
PyPI
pip install turbodrf
pip install turbodrf[fast]
GitHub
pip install git+https://github.com/alexandercollins/turbodrf.git
Requirements
- Python >= 3.10
- Django >= 4.2
- Django REST Framework >= 3.14
Quick Start
1. Add to INSTALLED_APPS
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'django_filters',
'turbodrf',
'myapp',
]
2. Add the mixin to 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.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
published_date = models.DateField()
searchable_fields = ['title', 'author']
@classmethod
def turbodrf(cls):
return {
'fields': ['title', 'author', 'price', 'published_date']
}
3. Add the router
from django.contrib import admin
from django.urls import path, include
from turbodrf import urls as turbodrf_urls
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include(turbodrf_urls)),
]
4. Configure TurboDRF roles
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',
]
}
5. Extend User Model with Roles
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):
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,
'endpoint': 'books',
'fields': ['title', 'author'],
'public_access': False,
'lookup_field': 'pk',
'compiled': 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',
'author__publisher__name',
'tags__name',
]
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 = True
-
Django default permissions:
TURBODRF_USE_DEFAULT_PERMISSIONS = True
-
Role-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.read or .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
price for viewers, add price.read to at least one role (like admin)
How It Works
TurboDRF reads user.roles — a property that returns a list of role names:
User.add_to_class('roles', property(lambda self: [g.name for g in self.groups.all()]))
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
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
author on Book?
- Can user read
publisher on Author?
- Can user read
name on 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.
Tenancy & Row-Level Access
Multi-Tenant SaaS
class Project(models.Model, TurboDRFMixin):
workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
@classmethod
def turbodrf(cls):
return {
'tenant_field': 'workspace',
'owner_field': 'owner',
'bypass_owner_roles': ['manager', 'admin'],
'fields': ['title', 'workspace', 'owner'],
}
Plus project-wide settings:
TURBODRF_TENANT_MODEL = 'accounts.Workspace'
TURBODRF_TENANT_USER_FIELD = 'workspace'
Request Flow
A request GET /api/projects/ from Alice (member at ABC workspace) goes through:
- Permission gate — Alice's role
member has app.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, owner but maybe not all configured fields. Hidden ones are removed from the response.
Quick Recipes
{'tenant_field': 'store', 'owner_field': 'customer', 'bypass_owner_roles': ['staff']}
{'owner_field': 'author', 'bypass_owner_roles': ['admin']}
{'tenancy': 'shared'}
{'visibility': [Tenant('workspace'), Members('participants')]}
{'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.
Optional: Postgres RLS
For Postgres deployments, TurboDRF can generate Row Level Security policies that enforce the same rules at the database layer. See docs/rls.md.
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 includes startup gates that detect:
- Compiled M2M target bypass vulnerabilities
- Compiled FK annotation bypass
- Search field target bypass
- Unsafe Custom predicate write validators
- Permission string typos
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
python manage.py turbodrf_check
python manage.py turbodrf_benchmark
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_SENSITIVE_FIELDS | Fields to hide from all users |
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, on_delete=models.CASCADE)
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 import urls as turbodrf_urls
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include(turbodrf_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:
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:
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
roles property
Compiled Path Safety Issues
If startup gate fires due to M2M/FK traversals:
- Drop the path from the parent's
turbodrf() fields list
- Set
'compiled': False on the parent model
- Strip the target's row-level rules if genuinely public
TURBODRF_ALLOW_UNSAFE_COMPILED_M2M = True (not recommended)
References