Skip to main content

TurboDRF

Python Version Django Version DRF Version License Tests Coverage PyPI Version PRs Welcome Code Style

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.

FeaturesQuick StartDocumentationExamplesContributing


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_related for 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:

  1. If field has explicit permissions → check those
  2. Else → fall back to model-level permissions
  3. Read: needs field.read OR model.read
  4. Write: needs field.write OR model.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

  1. Use different fields for list/detail views

    'fields': {
        'list': ['id', 'title'],      # Minimal
        'detail': '__all__'            # Complete
    }
    
  2. Add database indexes to filtered fields

    class Meta:
        indexes = [
            models.Index(fields=['title']),
            models.Index(fields=['published_date']),
        ]
    
  3. TurboDRF automatically optimizes queries with select_related for 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 __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/.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

turbodrf-0.2.3.tar.gz (112.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

turbodrf-0.2.3-py3-none-any.whl (62.9 kB view details)

Uploaded Python 3

File details

Details for the file turbodrf-0.2.3.tar.gz.

File metadata

  • Download URL: turbodrf-0.2.3.tar.gz
  • Upload date:
  • Size: 112.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for turbodrf-0.2.3.tar.gz
Algorithm Hash digest
SHA256 00163658a7e4d8be5312f240373338f6ed7e27bbc269de5c3f9ae32f69edfd63
MD5 4af9c7fcc31ab51c1800dd7e8aba8c2b
BLAKE2b-256 0f35c7551ed0194089c40d11377885a08eef47b21c3e252889cab7c790d17db3

See more details on using hashes here.

Provenance

The following attestation bundles were made for turbodrf-0.2.3.tar.gz:

Publisher: publish.yml on AlexanderCollins/TurboDRF

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file turbodrf-0.2.3-py3-none-any.whl.

File metadata

  • Download URL: turbodrf-0.2.3-py3-none-any.whl
  • Upload date:
  • Size: 62.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for turbodrf-0.2.3-py3-none-any.whl
Algorithm Hash digest
SHA256 f459cb69ebcd7f0530729b41fe5b5e49ece50c3df0f869393a3bc31921ce3570
MD5 6ec8bded65d1d1b22b41dbd1befbc7f1
BLAKE2b-256 ad8f74f4aceadb9d9ce25d59a9ea5d5499bc7fb176ba1b5e05fff683f1ba5639

See more details on using hashes here.

Provenance

The following attestation bundles were made for turbodrf-0.2.3-py3-none-any.whl:

Publisher: publish.yml on AlexanderCollins/TurboDRF

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.5.1

2 files

0.5.0

2 files

0.4.4

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

This release

0.2.3 This release

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.13

2 files

0.1.12

2 files

0.1.11

2 files

0.1.10

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.4

2 files

0.1.2

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page